MicrosoftGraphMail
Microsoft Graph lets your app get authorized access to a user's Outlook mail data in a personal or organization account.
Email · Microsoft Graph Mail
Details
| ID | MicrosoftGraphMail |
|---|---|
| Provider | Microsoft |
| Category | |
| From Version | 5.0.0 |
| Docker Image | demisto/crypto:1.0.0.10120494 |
| Supported Modules | Agentix Cloud Runtime Security XSIAM EDR Cortex Cloud |
README
Microsoft Graph lets your app get authorized access to a user’s Outlook mail data in a personal or organization account.
This integration was integrated and tested with version v1 of Microsoft Graph.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure O365 Outlook Mail (Using Graph API) in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | True | |
| Application ID or Client ID | See the Help tab. | False |
| Token or Tenant ID | See the Help tab. | False |
| Key or Client Secret | See the Help tab. | False |
| Certificate Thumbprint | Used for certificate authentication. As appears in the “Certificates & secrets” page of the app. | False |
| Private Key | Used for certificate authentication. The private key of the registered certificate. | False |
| Use Azure Managed Identities | Relevant only if the integration is running on Azure VM. If selected, authenticates based on the value provided for the Azure Managed Identities Client ID field. If no value is provided for the Azure Managed Identities Client ID field, authenticates based on the System Assigned Managed Identity. For additional information, see the Help tab. | False |
| Azure Managed Identities Client ID | The Managed Identities client ID for authentication - relevant only if the integration is running on Azure VM. | False |
| Fetch incidents | Whether to fetch incidents. | False |
| Email address from which to fetch incidents | For example, “example@demisto.com” | False |
| Name of the folder or sub-folder from which to fetch incidents | Supports folder ID and sub-folders, for example Inbox/Phishing. | False |
| First fetch timestamp | <number> /<time unit>, for example 12 hours, 7 days. | False |
| HTTP Timeout | The timeout of the HTTP requests sent to Microsoft Graph API (in seconds). | False |
| Maximum number of emails to pull per fetch | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Use a self deployed Azure application | False | |
| Incident type | False | |
| ID or Client ID - see Detailed Instructions (?) | False | |
| Token or Tenant ID - see Detailed Instructions (?) | False | |
| Key or Client Secret (Deprecated) | False | |
| ID or Client ID - see Detailed Instructions (?) (Deprecated) | False | |
| Token or Tenant ID - see Detailed Instructions (?) (Deprecated) | False | |
| Display full email body | If not active, only a preview of the email will be fetched. | |
| Mark fetched emails as read | Relevant only if fetch incidents is active. | |
| Advanced: Time in minutes to look back when fetching emails | Use this parameter to determine how far backward to look in the search for incidents that were created before the last run time and did not match the query when they were created. | False |
Required Permissions
The following permissions are required for all commands:
- Mail.ReadWrite - Application
- Mail.Send - Application
- MailboxSettings.ReadWrite - Application
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
msgraph-mail-list-emails
Gets the properties of returned emails. Typically shows partial results, use the “page_size” and “pages_to_pull” arguments to get all results.
Base Command
msgraph-mail-list-emails
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID from which to pull mails (can be principal ID (email address)). | Required |
| folder_id | A comma-separated list of folder IDs, in the format: (mail_box,child_mail_box,child_mail_box). . | Optional |
| odata | An OData query. See REDAME for OData usage examples. | Optional |
| search | The term for which to search. This argument cannot contain reserved characters such as !, $, #, @, etc. For further information, see https://tools.ietf.org/html/rfc3986#section-2.2. | Optional |
| page_size | Limit emails to fetch in one request. Default is 20. | Optional |
| pages_to_pull | The number of pages of emails to return (maximum is 10 emails per page). Default is 1. | Optional |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.ID | String | The ID of the email. |
| MSGraphMail.Created | Date | The time the email was created. |
| MSGraphMail.LastModifiedTime | Date | The time the email was last modified. |
| MSGraphMail.ReceivedTime | Date | The time the email was received. |
| MSGraphMail.SendTime | Date | The time the email was sent. |
| MSGraphMail.Categories | String | Categories of the email. |
| MSGraphMail.HasAttachments | Boolean | Whether the email has attachments. |
| MSGraphMail.Subject | String | The subject of email. |
| MSGraphMail.IsDraft | Boolean | Whether the email is a draft. |
| MSGraphMail.Body | String | The content (body) of the email. |
| MSGraphMail.Sender.Name | String | The name of sender. |
| MSGraphMail.Sender.Address | String | The email address of the sender. |
| MSGraphMail.From.Name | String | The name of the user in the ‘from’ field of the email. |
| MSGraphMail.From.Address | String | The email address of the user in the ‘from’ field of the email. |
| MSGraphMail.CCRecipients.Name | String | The names of the CC recipients. |
| MSGraphMail.CCRecipients.Address | String | The email address of the user in the ‘cc’ field of the email. |
| MSGraphMail.BCCRecipients.Name | String | The names of the users in the ‘bcc’ field of the email. |
| MSGraphMail.BCCRecipients.Address | String | The email address of the user in the ‘bcc’ field of the email. |
| MSGraphMail.ReplyTo.Name | String | The name in the ‘replyTo’ field of the email. |
| MSGraphMail.ReplyTo.Address | String | The email address in the ‘replyTo’ field of the email. |
| MSGraphMail.UserID | String | The ID of the user. |
| MSGraphMail.ConversationID | String | The ID of the conversation. |
| MSGraphMail.InternetMessageID | String | Internet Message ID of the message. |
| MSGraphMail.Recipients.Name | String | The name of the user in the ‘toRecipients’ field of the email. |
| MSGraphMail.Recipients.Address | String | The email address of the user in the ‘toRecipients’ field of the email. |
| MSGraphMail.NextPage | String | A token to pass to the next list command to retrieve additional results. |
msgraph-mail-get-email
Returns the properties of an email.
Base Command
msgraph-mail-get-email
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| message_id | The unique Exchange ID of the mail. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| folder_id | The folder ID. | Optional |
| odata | An OData query. See the README for OData usage examples. | Optional |
| get_body | Whether to return the message body. Possible values are: true, false. | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.ID | String | The ID of the email. |
| MSGraphMail.Created | Date | The time the email was created. |
| MSGraphMail.LastModifiedTime | Date | The time the email was last modified. |
| MSGraphMail.ReceivedTime | Date | The time the email was received. |
| MSGraphMail.SendTime | Date | The time the email was sent. |
| MSGraphMail.Categories | String | Categories of the email. |
| MSGraphMail.HasAttachments | Boolean | Whether the email has attachments. |
| MSGraphMail.Subject | String | The subject of email. |
| MSGraphMail.IsDraft | Boolean | Whether the email is a draft. |
| MSGraphMail.Body | String | The content (body) of the email. |
| MSGraphMail.Sender.Name | String | The name of sender. |
| MSGraphMail.Sender.Address | String | The email address of the sender. |
| MSGraphMail.From.Name | String | The name of the user in the ‘from’ field of the email. |
| MSGraphMail.From.Address | String | The email address of the user in the ‘from’ field of the email. |
| MSGraphMail.CCRecipients.Name | String | The names of the users in the ‘cc’ field of the email. |
| MSGraphMail.CCRecipients.Address | String | The email address of the user in the ‘cc’ field of the email. |
| MSGraphMail.BCCRecipients.Name | String | The names of the users in the ‘bcc’ field of the email. |
| MSGraphMail.BCCRecipients.Address | String | The email address of the user in the ‘bcc’ field of the email. |
| MSGraphMail.ReplyTo.Name | String | The name in the ‘replyTo’ field of the email. |
| MSGraphMail.ReplyTo.Address | String | The email address in the ‘replyTo’ field of the email. |
| MSGraphMail.UserID | String | The ID of the user. |
| MSGraphMail.ConversationID | String | The ID of the conversation. |
| MSGraphMail.InternetMessageID | String | Internet Message ID of the message. |
| MSGraphMail.Recipients.Name | String | The name of the user in the ‘toRecipients’ field of the email. |
| MSGraphMail.Recipients.Address | String | The email address of the user in the ‘toRecipients’ field of the email. |
msgraph-mail-delete-email
Deletes an email.
Base Command
msgraph-mail-delete-email
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| message_id | The unique Exchange ID of the mail. This could be extracted from - msgraph-mail-list-emails command results. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| folder_id | A comma-separated list of folder IDs. For example, mailFolders,childFolders,childFolders. | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
There is no context output for this command.
msgraph-mail-list-attachments
Lists all of the attachments of given email
Base Command
msgraph-mail-list-attachments
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| message_id | The unique Exchange ID of the mail. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| folder_id | A comma-separated list of folder IDs, in the format: (mail_box,child_mail_box,child_mail_box). | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMailAttachment.ID | String | The email ID. |
| MSGraphMailAttachment.Attachment.ID | String | The ID of the attachment. |
| MSGraphMailAttachment.Attachment.Name | String | The name of the attachment. |
| MSGraphMailAttachment.Attachment.Type | String | The attachment type. |
| MSGraphMailAttachment.UserID | String | The ID of the user. |
msgraph-mail-get-attachment
Gets an attachment from the email.
Base Command
msgraph-mail-get-attachment
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| message_id | The unique Exchange ID of the mail. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| folder_id | A comma-separated list of folder IDs, in the format: (mail_box,child_mail_box,child_mail_box). | Optional |
| attachment_id | The ID of the attachment. In case it is not supplied, the command will return all the attachments. | Optional |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| File.Size | Number | The size of the file. |
| File.SHA1 | String | The SHA1 hash of the file. |
| File.SHA256 | String | The SHA256 hash of the file. |
| File.Name | String | The name of the file. |
| File.SSDeep | String | The SSDeep hash of the file. |
| File.EntryID | String | The entry ID of the file. |
| File.Info | String | File information. |
| File.Type | String | The file type. |
| File.MD5 | String | The MD5 hash of the file. |
| File.Extension | String | The file extension. |
msgraph-mail-list-folders
Returns the mail folder list directly under the root folder.
Base Command
msgraph-mail-list-folders
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| limit | The maximum number of mail folder lists to return. Default is 20. | Optional |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Folders.ChildFolderCount | Number | The number of child folders. |
| MSGraphMail.Folders.DisplayName | String | The folder display name. |
| MSGraphMail.Folders.ID | String | The target folder ID. |
| MSGraphMail.Folders.ParentFolderID | String | The parent folder ID. |
| MSGraphMail.Folders.TotalItemCount | Number | The total number of email messages in the folder. |
| MSGraphMail.Folders.UnreadItemCount | Number | The number of unread emails in the folder. |
msgraph-mail-list-child-folders
Returns the folder list under the specified folder.
Base Command
msgraph-mail-list-child-folders
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| parent_folder_id | The ID of the parent folder. | Required |
| limit | The maximum number of mail folder lists to return. Default is 20. | Optional |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Folders.ChildFolderCount | Number | The number of child folders. |
| MSGraphMail.Folders.DisplayName | String | The folder display name. |
| MSGraphMail.Folders.ID | String | The folder ID. |
| MSGraphMail.Folders.ParentFolderID | String | The parent folder ID. |
| MSGraphMail.Folders.TotalItemCount | Number | The total number of email messages in the folder. |
| MSGraphMail.Folders.UnreadItemCount | Number | The number of unread email messages in the folder. |
msgraph-mail-create-folder
Creates a new folder under the specified folder (parent).
Base Command
msgraph-mail-create-folder
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| new_folder_name | The display name of the new folder. | Required |
| parent_folder_id | The ID of the parent folder under which to create a new folder. | Optional |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Folders.ChildFolderCount | Number | The number of child folders. |
| MSGraphMail.Folders.DisplayName | String | The folder display name. |
| MSGraphMail.Folders.ID | String | The folder ID. |
| MSGraphMail.Folders.ParentFolderID | String | The parent folder ID. |
| MSGraphMail.Folders.TotalItemCount | Number | The total number of email messages in the folder. |
| MSGraphMail.Folders.UnreadItemCount | Number | The number of unread email messages in the folder. |
msgraph-mail-update-folder
Updates the properties of the specified folder.
Base Command
msgraph-mail-update-folder
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| folder_id | The ID of the folder to update. | Required |
| new_display_name | The mail folder display name. | Required |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Folders.ChildFolderCount | String | The number of child folders. |
| MSGraphMail.Folders.DisplayName | String | The folder display name. |
| MSGraphMail.Folders.ID | String | The folder ID. |
| MSGraphMail.Folders.ParentFolderID | String | The parent folder ID. |
| MSGraphMail.Folders.TotalItemCount | Number | The total number of email messages in the folder. |
| MSGraphMail.Folders.UnreadItemCount | Number | The unread emails count inside the folder. |
msgraph-mail-delete-folder
Deletes the specified mail folder.
Base Command
msgraph-mail-delete-folder
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| folder_id | The ID of the folder to delete. | Required |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
There is no context output for this command.
msgraph-mail-move-email
Moves a message to a different folder.
Base Command
msgraph-mail-move-email
Input
| Argument Name | Description | Required |
|---|---|---|
| message_id | The unique Exchange ID of the mail. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| destination_folder_id | The ID of the destination folder. | Required |
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.MovedEmails.DestinationFolderID | String | The folder where the email message was moved. |
| MSGraphMail.MovedEmails.ID | String | The new ID of the moved email message. |
| MSGraphMail.MovedEmails.UserID | String | The user ID. |
msgraph-mail-get-email-as-eml
Retrieves an email message by message ID and uploads the content as an EML file.
Base Command
msgraph-mail-get-email-as-eml
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| message_id | The unique Exchange ID of the mail. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. | Required |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| File.Size | String | The size of the file. |
| File.SHA1 | String | The SHA1 hash of the file. |
| File.SHA256 | String | The SHA256 hash of the file. |
| File.SHA512 | String | The SHA512 hash of the file. |
| File.Name | String | The name of the file. |
| File.SSDeep | String | The SSDeep hash of the file. |
| File.EntryID | String | The 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. |
msgraph-mail-create-draft
Creates a draft message in the specified user’s mailbox.
Base Command
msgraph-mail-create-draft
Input
| Argument Name | Description | Required |
|---|---|---|
| to | A comma-separated list of email addresses for the ‘to’ field. | Optional |
| 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 | The subject for the draft. | Required |
| body | The contents (body) of the draft. | Optional |
| bodyType | The body type of the email. Can be: “text”, or “HTML”. Possible values are: text, HTML. Default is text. | Optional |
| flag | The flag value that indicates the status of the draft. Can be: “notFlagged”, “complete”, or “flagged”. Possible values are: notFlagged, complete, flagged. Default is notFlagged. | Optional |
| importance | The importance of the draft. Can be: “Low”, “Normal”, or “High”. Possible values are: Low, Normal, High. Default is Low. | Optional |
| headers | A comma-separated list of additional headers in the format, headerName:headerValue. For example, “headerName1:headerValue1,headerName2:headerValue2”. | Optional |
| attachIDs | A comma-separated list of War Room entry IDs that contain files, which are used to attach files to the draft. For example, attachIDs=15@8,19@8. | Optional |
| attachNames | A comma-separated list of names of attachments to be displayed in the draft. Must be the same number of elements as attachIDs. | Optional |
| attachCIDs | A comma-separated list of CIDs to embed attachments within the actual email. | Optional |
| from | The email address from which the draft is created. | Required |
| ran_once_flag | flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MicrosoftGraph.Draft.Cc | String | The CC recipients of the draft email. |
| MicrosoftGraph.Draft.IsRead | String | The “Is read” status of the draft email. |
| MicrosoftGraph.Draft.Bcc | String | The BCC recipients of the draft email. |
| MicrosoftGraph.Draft.Body | String | The body of the draft email. |
| MicrosoftGraph.Draft.MessageID | String | The message ID of the draft email. |
| MicrosoftGraph.Draft.SentTime | Date | The created time of the draft email. |
| MicrosoftGraph.Draft.Headers | String | The headers of the draft email. |
| MicrosoftGraph.Draft.From | String | The user that sent the draft email. |
| MicrosoftGraph.Draft.Subject | String | The subject of the draft email. |
| MicrosoftGraph.Draft.ReceivedTime | String | The received time of the draft email. |
| MicrosoftGraph.Draft.Importance | String | The importance status of the draft email. |
| MicrosoftGraph.Draft.CreatedTime | String | The created time of the draft email. |
| MicrosoftGraph.Draft.Sender | String | The sender of the draft email. |
| MicrosoftGraph.Draft.ModifiedTime | Date | The modified time of the draft email. |
| MicrosoftGraph.Draft.IsDraft | Boolean | Whether it is a draft email. |
| MicrosoftGraph.Draft.ID | String | The ID of the draft email. |
| MicrosoftGraph.Draft.To | String | The ‘to’ recipients of the draft email. |
| MicrosoftGraph.Draft.BodyType | Unknown | The body type of the draft email. |
| MicrosoftGraph.Draft.ConversationID | String | The conversation ID of the draft email. |
send-mail
Sends an email using Microsoft Graph.
Note: The from argument needs to be specified when the Email address from which to fetch incidents parameter is missing.
Base Command
send-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| to | A comma-separated list of email addresses for the ‘to’ field. | Optional |
| 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 | The subject of the email. | Required |
| body | The contents (body) of the email. | Optional |
| bodyType | The body type of the email. Possible values are: text, HTML. | Optional |
| flag | The flag value that indicates the status for the email. Possible values are: notFlagged, complete, flagged. Default is notFlagged. | Optional |
| importance | The importance of the email. Possible values are: Low, Normal, High. Default is Low. | Optional |
| headers | A comma-separated list of additional headers in the format: headerName:headerValue. For example: “headerName1:headerValue1,headerName2:headerValue2”. | Optional |
| attachIDs | A comma-separated list of War Room entry IDs that contain files, which are used to attach files for the email to send. For example, attachIDs=15@8,19@8. | Optional |
| attachNames | A comma-separated list of names of attachments to display in the email to send. Must be the same number of elements as attachIDs. | Optional |
| attachCIDs | A comma-separated list of CIDs to embed attachments within the actual email. | Optional |
| from | The email address from which to send the email. | Optional |
| htmlBody | The content (body) of the email (in HTML format). | Optional |
| replyTo | Email addresses that need to be used to reply to the message. Supports comma-separated values. | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MicrosoftGraph.Email.internetMessageHeaders | String | The email headers. |
| MicrosoftGraph.Email.body | String | The body of the email. |
| MicrosoftGraph.Email.bodyPreview | String | The body preview of the email. |
| MicrosoftGraph.Email.subject | String | The subject of the email. |
| MicrosoftGraph.Email.flag | String | The flag status of the email. |
| MicrosoftGraph.Email.importance | String | The importance status of the email. |
| MicrosoftGraph.Email.toRecipients | String | The ‘to’ recipients of the email. |
| MicrosoftGraph.Email.ccRecipients | String | The CC recipients of the email. |
| MicrosoftGraph.Email.bccRecipients | String | The BCC recipients of the email. |
| MicrosoftGraph.Email.replyTo | String | The replyTo recipients of the email. |
msgraph-mail-reply-to
The replies to the recipients of a message.
Base Command
msgraph-mail-reply-to
Input
| Argument Name | Description | Required |
|---|---|---|
| ID | The ID of the message. | Required |
| body | The comment of the replied message. | Required |
| to | A comma-separated list of email addresses for the ‘to’ field. | Required |
| from | The email address from which to reply. | Required |
| attachIDs | A comma-separated list of War Room entry IDs that contain files, which are used to attach files for the email to send. For example, attachIDs=15@8,19@8. | Optional |
| attachNames | A comma-separated list of names of attachments to display in the email to send. Must be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV list of CIDs to embed attachments within the email itself. | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
There is no context output for this command.
msgraph-mail-send-draft
Sends a draft email using Microsoft Graph.
Base Command
msgraph-mail-send-draft
Input
| Argument Name | Description | Required |
|---|---|---|
| draft_id | The ID of the draft email. | Required |
| from | The email address from which to send the draft. | Required |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
There is no context output for this command.
reply-mail
Replies to an email using Graph Mail.
Base Command
reply-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| to | A CSV list of email addresses for the ‘to’ field. | Required |
| body | The contents (body) of the email to be sent. | Optional |
| subject | Subject for the email to be sent. | Required |
| inReplyTo | ID of the item to reply to. | Required |
| attachIDs | A CSV list of War Room entry IDs that contain files, and are used to attach files to the outgoing email. For example: attachIDs=15@8,19@8. | Optional |
| cc | A CSV list of email addresses for the ‘cc’ field. | Optional |
| bcc | A CSV list of email addresses for the ‘bcc’ field. | Optional |
| htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the “body” argument. | Optional |
| attachNames | A CSV list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV list of CIDs to embed attachments within the email itself. | Optional |
| from | Email address of the sender. | Optional |
| replyTo | Email addresses that need to be used to reply to the message. Supports comma-separated values. | Optional |
| ran_once_flag | Flag for rate limit retry. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MicrosoftGraph.SentMail.body | String | The body of the email. |
| MicrosoftGraph.SentMail.bodyPreview | String | The body preview of the email. |
| MicrosoftGraph.SentMail.subject | String | The subject of the email. |
| MicrosoftGraph.SentMail.toRecipients | String | The ‘To’ recipients of the email. |
| MicrosoftGraph.SentMail.ccRecipients | String | The CC recipients of the email. |
| MicrosoftGraph.SentMail.bccRecipients | String | The BCC recipients of the email. |
| MicrosoftGraph.SentMail.ID | String | The immutable ID of the message. |
| MicrosoftGraph.SentMail.replyTo | String | The replyTo recipients of the email. |
msgraph-mail-update-email-status
Update the status of an email to read / unread.
Base Command
msgraph-mail-update-email-status
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address. E.g., user@example.com). | Required |
| message_ids | The unique Exchange IDs of the mails to update. This is not the same as the ‘MessageID’ key in the form ‘<message-id>’. Can be a list of comma-separated values. | Required |
| folder_id | The folder ID. | Optional |
| status | Status to set the email to. Possible values are: Read, Unread. | Required |
Context Output
There is no context output for this command.
msgraph-mail-auth-reset
Run this command if for some reason you need to rerun the authentication process.
Base Command
msgraph-mail-auth-reset
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
msgraph-mail-list-rules
List email rules for a user’s mailbox using Microsoft Graph API.
Base Command
msgraph-mail-list-rules
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| limit | Maximum number of results to return. Default is 50. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Rule.conditions | Unknown | Conditions that when fulfilled, will trigger the corresponding actions for that rule. |
| MSGraphMail.Rule.actions | Unknown | Actions to be taken on a message when the corresponding conditions are fulfilled. |
| MSGraphMail.Rule.displayName | String | The display name of the rule. |
| MSGraphMail.Rule.exceptions | Unknown | Exception conditions for the rule. |
| MSGraphMail.Rule.hasError | Boolean | Indicates whether the rule is in an error condition. |
| MSGraphMail.Rule.id | String | The ID of the rule. |
| MSGraphMail.Rule.isEnabled | Boolean | Indicates whether the rule is enabled to be applied to messages. |
| MSGraphMail.Rule.isReadOnly | Boolean | Indicates if the rule is read-only and cannot be modified or deleted by the rules REST API. |
| MSGraphMail.Rule.sequence | Number | Indicates the order in which the rule is executed, among other rules. |
msgraph-mail-get-rule
Get details of a specific email rule by ID for a user’s mailbox using Microsoft Graph API.
Base Command
msgraph-mail-get-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| rule_id | The ID of the rule to retrieve. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Rule.conditions | Unknown | Conditions that when fulfilled, will trigger the corresponding actions for that rule. |
| MSGraphMail.Rule.actions | Unknown | Actions to be taken on a message when the corresponding conditions are fulfilled. |
| MSGraphMail.Rule.displayName | String | The display name of the rule. |
| MSGraphMail.Rule.exceptions | Unknown | Exception conditions for the rule. |
| MSGraphMail.Rule.hasError | Boolean | Indicates whether the rule is in an error condition. |
| MSGraphMail.Rule.id | String | The ID of the rule. |
| MSGraphMail.Rule.isEnabled | Boolean | Indicates whether the rule is enabled to be applied to messages. |
| MSGraphMail.Rule.isReadOnly | Boolean | Indicates if the rule is read-only and cannot be modified or deleted by the rules REST API. |
| MSGraphMail.Rule.sequence | Number | Indicates the order in which the rule is executed, among other rules. |
msgraph-mail-delete-rule
Deletes a specific email rule by ID from a user’s mailbox using the Microsoft Graph API.
Base Command
msgraph-mail-delete-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | User ID or principal ID (usually an email address in the format someuser@example.com). | Required |
| rule_id | The ID of the rule to delete. | Required |
Context Output
There is no context output for this command.
msgraph-mail-get-settings
Retrieves the user’s mailbox settings, including automatic replies, time zone, locale, working hours, date/time formats, archive folder, and user purpose.
Base Command
msgraph-mail-get-settings
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The ID or User Principal Name (UPN) of the user that owns the mailbox. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.MailboxSettings.userId | String | The ID of the user whose mailbox settings were retrieved (mirrors the user_id input). |
| MSGraphMail.MailboxSettings.archiveFolder | String | The folder ID of an archive folder for the user. |
| MSGraphMail.MailboxSettings.dateFormat | String | The date format for the user’s mailbox. |
| MSGraphMail.MailboxSettings.delegateMeetingMessageDeliveryOptions | String | The delivery method for meeting requests, updates, and responses are delivered when the user is a delegate. Possible values: sendToDelegateAndInformationToPrincipal, sendToDelegateAndPrincipal, sendToDelegateOnly. |
| MSGraphMail.MailboxSettings.timeFormat | String | The time format for the user’s mailbox. |
| MSGraphMail.MailboxSettings.timeZone | String | The default time zone for the user’s mailbox. |
| MSGraphMail.MailboxSettings.userPurpose | String | The purpose of the mailbox. Possible values: unknown, user, linked, shared, room, equipment, others. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.status | String | The status of the automatic reply (out-of-office) feature. Possible values: disabled, alwaysEnabled, scheduled. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.externalAudience | String | The audience that receives the external replies. Possible values: none, contactsOnly, all. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.internalReplyMessage | String | The automatic reply sent to senders within the user’s organization. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.externalReplyMessage | String | The automatic reply sent to senders outside the user’s organization. Sent only if externalAudience is contactsOnly or all. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.scheduledStartDateTime.dateTime | Date | The start date and time automatic replies are sent (ISO 8601 format, for example 2020-01-01T00:11:22Z)). |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.scheduledStartDateTime.timeZone | String | The time zone of the scheduledStartDateTime. |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.scheduledEndDateTime.dateTime | Date | The end date and time automatic replies are sent (ISO 8601 format, for example 2020-01-01T00:11:22Z)). |
| MSGraphMail.MailboxSettings.automaticRepliesSetting.scheduledEndDateTime.timeZone | String | The time zone of the scheduledEndDateTime. |
| MSGraphMail.MailboxSettings.language.locale | String | The language locale (BCP 47 culture name). For example, en-US. |
| MSGraphMail.MailboxSettings.language.displayName | String | The display name of the language. For example, “English (United States)”. |
| MSGraphMail.MailboxSettings.workingHours.daysOfWeek | Unknown | The days of the week the user works. Possible values: sunday, monday, tuesday, wednesday, thursday, friday, saturday. |
| MSGraphMail.MailboxSettings.workingHours.startTime | String | The time of day the user starts work. |
| MSGraphMail.MailboxSettings.workingHours.endTime | String | The time of day the user stops work. |
| MSGraphMail.MailboxSettings.workingHours.timeZone.name | String | The time zone name to which the working hours apply. |
msgraph-mail-create-rule
Creates a new mailbox rule in the user’s Inbox folder. By default, rules are created as disabled and if no sequence is defined, assigned to the first position in the sequence.
Base Command
msgraph-mail-create-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The ID or User Principal Name (UPN) of the user that owns the mailbox. | Required |
| display_name | The display name of the rule. | Required |
| sequence | The execution order of the rule among other rules. Must be a positive integer greater than or equal to 1. Lower numbers run first. If the specified sequence is already used by an existing rule, the new rule takes that sequence and the existing rule, along with any following rules, is shifted up by 1. Default is 1. | Optional |
| actions | The actions to perform when the rule conditions are met, in JSON format. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messageruleactions. Example: {"forwardTo":[{"emailAddress":{"address":"alex@contoso.com"}}],"stopProcessingRules":true}. |
Required |
| conditions | The conditions that trigger the rule’s actions, in JSON format. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messagerulepredicates. Example: {"senderContains":["adele"],"subjectContains":["urgent"]}. |
Optional |
| exceptions | The exception conditions for the rule, in JSON format. Replaces existing rule exceptions. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messagerulepredicates. Example: {"fromAddresses":[{"emailAddress":{"address":"trusted@contoso.com"}}]}. |
Optional |
| is_enabled | Whether the rule is enabled to be applied to messages. Default is false (disabled). Possible values are: true, false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Rule.id | String | The unique identifier of the rule. |
| MSGraphMail.Rule.displayName | String | The display name of the rule. |
| MSGraphMail.Rule.sequence | Number | The order in which the rule is executed relative to other rules. |
| MSGraphMail.Rule.isEnabled | Boolean | Whether the rule is enabled to be applied to messages. |
| MSGraphMail.Rule.isReadOnly | Boolean | Whether the rule is read-only and cannot be modified or deleted by the rule’s REST API. |
| MSGraphMail.Rule.hasError | Boolean | Whether the rule is in an error condition. |
| MSGraphMail.Rule.conditions | Unknown | The conditions that trigger the rule’s actions. |
| MSGraphMail.Rule.actions | Unknown | The actions to perform on a message when the rule conditions are met. |
| MSGraphMail.Rule.exceptions | Unknown | The exception conditions for the rule. |
msgraph-mail-update-rule
Updates an existing mailbox rule in the user’s Inbox folder by ID. Only the supplied fields are sent in the PATCH body; other fields remain unchanged.
Base Command
msgraph-mail-update-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The ID or User Principal Name (UPN) of the user that owns the mailbox. | Required |
| rule_id | The ID of the rule to update. | Required |
| display_name | The new display name of the rule. | Optional |
| sequence | The execution order of the rule among other rules. Must be a positive integer greater than or equal to 1. Lower numbers run first. If the specified sequence is already used by an existing rule, the new rule takes that sequence and the existing rule, along with any following rules, is shifted up by 1. | Optional |
| is_enabled | Whether the rule is enabled to be applied to messages. Possible values are: true, false. | Optional |
| actions | The actions to perform when the rule conditions are met, in JSON format. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messageruleactions. Example: {"forwardTo":[{"emailAddress":{"address":"alex@contoso.com"}}],"stopProcessingRules":true}. |
Optional |
| conditions | The conditions that trigger the rule’s actions, in JSON format. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messagerulepredicates. Example: {"senderContains":["adele"],"subjectContains":["urgent"]}. |
Optional |
| exceptions | The exception conditions for the rule, in JSON format. Replaces existing rule exceptions. For a list of the available fields, see: https://learn.microsoft.com/en-us/graph/api/resources/messagerulepredicates. Example: {"fromAddresses":[{"emailAddress":{"address":"trusted@contoso.com"}}]}. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.Rule.id | String | The unique identifier of the rule. |
| MSGraphMail.Rule.displayName | String | The display name of the rule. |
| MSGraphMail.Rule.sequence | Number | The order in which the rule is executed relative to other rules. |
| MSGraphMail.Rule.isEnabled | Boolean | Whether the rule is enabled to be applied to messages. |
| MSGraphMail.Rule.isReadOnly | Boolean | Whether the rule is read-only and cannot be modified or deleted by the rule’s REST API. |
| MSGraphMail.Rule.hasError | Boolean | Whether the rule is in an error condition. |
| MSGraphMail.Rule.conditions | Unknown | The conditions that trigger the rule’s actions. |
| MSGraphMail.Rule.actions | Unknown | The actions to perform on a message when the rule conditions are met. |
| MSGraphMail.Rule.exceptions | Unknown | The exception conditions for the rule. |
msgraph-mail-get-mailtips
Retrieves Mail Tips for the specified email address, including out-of-office status, mailbox-full status, max message size, delivery restrictions, recipient scope, custom mail tip, and group membership counts.
Base Command
msgraph-mail-get-mailtips
Input
| Argument Name | Description | Required |
|---|---|---|
| email_address | The email address (User Principal Name / UPN) of the mailbox to retrieve Mail Tips for. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| MSGraphMail.MailTips.emailAddress.name | String | The display name of the recipient. |
| MSGraphMail.MailTips.emailAddress.address | String | The SMTP address of the recipient. |
| MSGraphMail.MailTips.emailAddressValue | String | The email address of the recipient (flattened key used for context merging). |
| MSGraphMail.MailTips.automaticReplies.message | String | The automatic reply message. |
| MSGraphMail.MailTips.automaticReplies.messageLanguage.locale | String | The language locale (BCP 47) of the automatic reply message. |
| MSGraphMail.MailTips.automaticReplies.messageLanguage.displayName | String | The display name of the language for the automatic reply message. |
| MSGraphMail.MailTips.mailboxFull | Boolean | Whether the recipient mailbox is full. |
| MSGraphMail.MailTips.customMailTip | String | A custom Mail Tip that has been configured for the recipient. |
| MSGraphMail.MailTips.externalMemberCount | Number | The number of external members in a group. |
| MSGraphMail.MailTips.totalMemberCount | Number | The number of members in a group. |
| MSGraphMail.MailTips.deliveryRestricted | Boolean | Whether delivery restrictions will prevent the sender’s message from reaching the recipient. |
| MSGraphMail.MailTips.isModerated | Boolean | Whether sending messages to the recipient requires approval. For example, if the recipient is a large distribution list and a moderator has been set up to approve messages sent to that list. |
| MSGraphMail.MailTips.recipientScope | String | The scope of the recipient. Possible values: none, internal, external, externalPartner, externalNonPartner. |
| MSGraphMail.MailTips.recipientSuggestions | Unknown | The recipients identified from previous contexts who appeared in the same message. |
| MSGraphMail.MailTips.maxMessageSize | Number | The maximum message size configured for the recipient’s organization or mailbox. |
| MSGraphMail.MailTips.error.code | String | The error code returned if the Mail Tips lookup for the recipient fails. |
| MSGraphMail.MailTips.error.message | String | The human-readable error message returned if the Mail Tips lookup for the recipient fails. |
Configuration parameters
url— Server URL (required)creds_auth_id—creds_tenant_id—credentials—creds_certificate— Certificate Thumbprintcertificate_thumbprint— Certificate Thumbprintprivate_key— Private Keyself_deployed— Use a self deployed Azure applicationuse_managed_identities— Use Azure Managed Identitiesmanaged_identities_client_id—isFetch— Fetch incidentsmailbox_to_fetch— Email address from which to fetch incidentsfolder_to_fetch— Name of the folder or sub-folder from which to fetch incidentsfirst_fetch— First fetch timestamptimeout— HTTP Timeoutfetch_limit— Maximum number of emails to pull per fetchincidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval_auth_id— ID or Client ID_tenant_id— Token or Tenant IDenc_key— Key or Client Secret (Deprecated)auth_id— ID or Client ID - see Detailed Instructions (?) (Deprecated)tenant_id— Token or Tenant ID - see Detailed Instructions (?) (Deprecated)display_full_email_body— Display full email bodymark_fetched_read— Mark fetched emails as readlook_back— Advanced: Time in minutes to look back when fetching emailsinsecure— Trust any certificate (not secure)proxy— Use system proxy settingslegacy_name— Use legacy attachment name
Commands (26)
-
msgraph-mail-auth-resetRun this command if for some reason you need to rerun the authentication process.
-
msgraph-mail-create-draftCreates a draft message in the specified user's mailbox.
-
msgraph-mail-create-folderCreates a new folder under the specified folder (parent).
-
msgraph-mail-create-ruleCreates a new mailbox rule in the user's Inbox folder. By default, rules are created as disabled and if no sequence is defined, assigned to the first position in the sequence.
-
msgraph-mail-delete-emailDeletes an email.
-
msgraph-mail-delete-folderDeletes the specified mail folder.
-
msgraph-mail-delete-ruleDeletes a specific email rule by ID from a user's mailbox using the Microsoft Graph API.
-
msgraph-mail-get-attachmentGets an attachment from the email.
-
msgraph-mail-get-emailReturns the properties of an email.
-
msgraph-mail-get-email-as-emlRetrieves an email message by message ID and uploads the content as an EML file.
-
msgraph-mail-get-mailtipsRetrieves Mail Tips for the specified email address, including out-of-office status, mailbox-full status, max message size, delivery restrictions, recipient scope, custom mail tip, and group membership counts.
-
msgraph-mail-get-ruleGet details of a specific email rule by ID for a user's mailbox using Microsoft Graph API.
-
msgraph-mail-get-settingsRetrieves the user's mailbox settings, including automatic replies, time zone, locale, working hours, date/time formats, archive folder, and user purpose.
-
msgraph-mail-list-attachmentsLists all of the attachments of given email.
-
msgraph-mail-list-child-foldersReturns the folder list under the specified folder.
-
msgraph-mail-list-emailsGets the properties of returned emails. Typically shows partial results, use the "page_size" and "pages_to_pull" arguments to get all results.
-
msgraph-mail-list-foldersReturns the mail folder list directly under the root folder.
-
msgraph-mail-list-rulesList email rules for a user's mailbox using Microsoft Graph API.
-
msgraph-mail-move-emailMoves a message to a different folder.
-
msgraph-mail-reply-toThe replies to the recipients of a message.
-
msgraph-mail-send-draftSends a draft email using Microsoft Graph.
-
msgraph-mail-update-email-statusUpdate the status of an email to read / unread.
-
msgraph-mail-update-folderUpdates the properties of the specified folder.
-
msgraph-mail-update-ruleUpdates an existing mailbox rule in the user's Inbox folder by ID. Only the supplied fields are sent in the PATCH body; other fields remain unchanged.
-
reply-mailReplies to an email using Graph Mail.
-
send-mailSends an email using Microsoft Graph.
import base64 from urllib.parse import quote import demistomock as demisto import pytest import requests_mock from CommonServerPython import * from MicrosoftApiModule import MicrosoftClient from MicrosoftGraphMail import * from MicrosoftGraphMailApiModule import parse_json_arg class MockedResponse: def __init__(self, status_code): self.status_code = status_code @pytest.mark.parametrize( "params, expected_result", [ ({"creds_tenant_id": {"password": "1234"}, "creds_auth_id": {"password": "1234"}}, "Key must be provided."), ({"creds_tenant_id": {"password": "1234"}, "credentials": {"password": "1234"}}, "ID must be provided."), ({"credentials": {"password": "1234"}, "creds_auth_id": {"password": "1234"}}, "Token must be provided."), ], ) def test_params(mocker, params, expected_result): """ Given: - Case 1: tenant id and auth id but no key. - Case 2: tenant id and key but no auth id. - Case 3: key and auth id but no tenant id. When: - Setting an instance Then: - Ensure the exception message as expected. - Case 1: Should return "Key must be provided.". - Case 2: Should return "ID must be provided.". - Case 3: Should return "Token must be provided.". """ import MicrosoftGraphMail mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(MicrosoftGraphMail, "return_error") main() assert expected_result in MicrosoftGraphMail.return_error.call_args[0][0] @pytest.mark.parametrize( "params, expected_results", [ ( {"creds_tenant_id": {"password": "1234"}, "creds_auth_id": {"password": "3124"}, "credentials": {"password": "2412"}}, ["1234", "3124", "2412"], ), ({"tenant_id": "5678", "enc_key": "8142", "auth_id": "5678"}, ["5678", "5678", "8142"]), ({"_tenant_id": "1267", "credentials": {"password": "1234"}, "_auth_id": "8888"}, ["1267", "8888", "1234"]), ], ) def test_params_working(mocker, params, expected_results): """ Given: - Case 1: tenant id, auth id and key where all three are part of the credentials. - Case 2: tenant id, auth id and key where all three aren't part of the credentials. - Case 3: tenant id, auth id and key where only the key is part of the credentials. When: - Setting an instance Then: - Ensure that the instance can Co-op with previous versions params names and that MsGraphMailClient.__init__ was called with the right tenant id, auth id and key. - Case 1: MsGraphMailClient.__init__ Should be called with tenant id, auth id and key extracted from credentials type params. - Case 2: MsGraphMailClient.__init__ Should be called with tenant id, auth id and key not extracted from credentials type params. - Case 3: MsGraphMailClient.__init__ Should be called with only key param extracted from credentials type params. """ mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(MsGraphMailBaseClient, "__init__", return_value=None) main() MsGraphMailBaseClient.__init__.assert_called_with( self_deployed=False, tenant_id=expected_results[0], auth_id=expected_results[1], enc_key=expected_results[2], app_name="ms-graph-mail", base_url="/v1.0", verify=True, proxy=False, ok_codes=(200, 201, 202, 204), mailbox_to_fetch="", folder_to_fetch="Inbox", first_fetch_interval="15 minutes", emails_fetch_limit=50, timeout=10, endpoint="com", certificate_thumbprint="", private_key="", display_full_email_body=False, mark_fetched_read=False, look_back=0, managed_identities_client_id=None, legacy_name=False, ) def test_build_mail_object(): # Testing list of mails user_id = "ex@example.com" with open("test_data/mails") as mail_json: mail = json.load(mail_json) res = GraphMailUtils.build_mail_object(mail, user_id=user_id, get_body=True) assert isinstance(res, list) assert len(mail[0].get("value")) == len(res) assert res[0]["Created"] == "2019-04-16T19:40:00Z" assert res[0]["UserID"] == user_id assert res[0]["Body"] with open("test_data/mail") as mail_json: mail = json.load(mail_json) res = GraphMailUtils.build_mail_object(mail, user_id=user_id, get_body=True) assert isinstance(res, dict) assert res["UserID"] == user_id assert res["Body"] def test_assert_pages(): assert GraphMailUtils.assert_pages(3) == 3 assert GraphMailUtils.assert_pages(None) == 1 assert GraphMailUtils.assert_pages("4") == 4 def test_build_folders_path(): inp = "i,s,f,q" response = GraphMailUtils.build_folders_path(inp) assert response == "mailFolders/i/childFolders/s/childFolders/f/childFolders/q" def oproxy_client(): auth_id = "dummy_auth_id" enc_key = "dummy_enc_key" token_retrieval_url = "url_to_retrieval" auth_and_token_url = f"{auth_id}@{token_retrieval_url}" app_name = "ms-graph-mail" mailbox_to_fetch = "dummy@mailbox.com" # disable-secrets-detection folder_to_fetch = "Phishing" first_fetch_interval = "20 minutes" emails_fetch_limit = 50 base_url = "https://graph.microsoft.com/v1.0" ok_codes = (200, 201, 202) return MsGraphMailClient( self_deployed=False, tenant_id="", auth_id=auth_and_token_url, enc_key=enc_key, app_name=app_name, base_url=base_url, verify=True, proxy=False, ok_codes=ok_codes, mailbox_to_fetch=mailbox_to_fetch, folder_to_fetch=folder_to_fetch, first_fetch_interval=first_fetch_interval, emails_fetch_limit=emails_fetch_limit, ) def self_deployed_client(): tenant_id = "dummy_tenant" client_id = "dummy_client_id" client_secret = "dummy_secret" mailbox_to_fetch = "dummy@mailbox.com" # disable-secrets-detection folder_to_fetch = "Phishing" first_fetch_interval = "20 minutes" emails_fetch_limit = 50 base_url = "https://graph.microsoft.com/v1.0" ok_codes = (200, 201, 202) return MsGraphMailClient( self_deployed=True, tenant_id=tenant_id, auth_id=client_id, enc_key=client_secret, base_url=base_url, verify=True, proxy=False, ok_codes=ok_codes, app_name="", mailbox_to_fetch=mailbox_to_fetch, folder_to_fetch=folder_to_fetch, first_fetch_interval=first_fetch_interval, emails_fetch_limit=emails_fetch_limit, ) @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_pages_puller(mocker, client): """Unit test Given - pages_puller function - different number of pages to pull When - mock the requests response. Then - run the pages_puller command using the Client Validate that the number of returned pages is according to the number of pages to pull """ first_response = {"@odata.context": "response_context", "@odata.nextLink": "link_1", "value": ["email1", "email2"]} second_response = {"@odata.context": "response_context", "@odata.nextLink": "link_2", "value": ["email3", "email4"]} responses = client.pages_puller(first_response, 1) assert len(responses) == 1 mocker.patch.object(client, "http_request", return_value=second_response) responses = client.pages_puller(first_response, 2) assert len(responses) == 2 @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_list_mails_command(mocker, client): """Unit test Given - list_mails command - different number of mails that are returned in the response When - mock the client.list_mails function Then - run the list_mails_command using the Client Validate that the human readable output, indicating the number of returned mails is correct """ args = {"user_id": "test id"} # call list mails with two emails with open("test_data/mails") as mail_json: mail = json.load(mail_json) mocker.patch.object(client, "list_mails", return_value=mail) mocker.patch.object(demisto, "results") result_entry = list_mails_command(client, args) hr = result_entry.get("HumanReadable") assert ( "2 mails received\nPay attention there are more results than shown. For more data please " 'increase "pages_to_pull" argument' in hr ) # call list mails with no emails with open("test_data/no_mails") as mail_json: mail = json.load(mail_json) mocker.patch.object(client, "list_mails", return_value=mail) mocker.patch.object(demisto, "results") command_result = list_mails_command(client, args) hr = command_result.readable_output assert "### No mails were found" in hr @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_list_mails_command_encoding(mocker, client): """Unit test Given - an email query When - calling list_mails Then - Validate that the queried value is properly url-encoded """ client = MsGraphMailClient( self_deployed=True, tenant_id="tenant", auth_id="auth_token_url", enc_key="enc_key", app_name="app_name", base_url="https://example.com", verify=True, proxy=False, ok_codes=(200,), mailbox_to_fetch="mailbox", folder_to_fetch="folder", first_fetch_interval=10, emails_fetch_limit=10, ) mocker.patch.object(client, "get_access_token") search = "Test&$%^" search_encoded = quote(search) with requests_mock.Mocker() as request_mocker: mocked = request_mocker.get(f"https://example.com/users/user_id/messages?$top=20&$search=%22{search_encoded}%22", json={}) client.list_mails("user_id", search=search) assert mocked.call_count == 1 @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_list_mails_with_page_limit(mocker, client): """Unit test Given - list_mails command with page_size set to 1 - one mail returned on the response When - mock the MicrosoftClient.http_request function Then - run the list_mails_command using the Client Validate that the http_request called properly with endpoint top=1 """ args = {"user_id": "test id", "page_size": 1, "pages_to_pull": 1} with open("test_data/response_with_one_mail") as mail_json: mail = json.load(mail_json) mock_request = mocker.patch.object(MicrosoftClient, "http_request", return_value=mail) mocker.patch.object(demisto, "results") mocker.patch.object(demisto, "args", return_value=args) result_entry = list_mails_command(client, args) hr = result_entry.get("HumanReadable") assert ( "1 mails received\nPay attention there are more results than shown. For more data please " 'increase "pages_to_pull" argument' in hr ) assert "top=1" in mock_request.call_args_list[0].args[1] def test_list_mails_with_body(): """ Given - list_mails command When - The mail has a body Then - Return the body. """ mail = [{"value": [{"body": {"content": "This is an email body"}}]}] client = type("MockClient", (), {"list_mails": lambda *x, **y: mail})() result_entry = list_mails_command(client, {}) assert result_entry["EntryContext"]["MSGraphMail(val.ID && val.ID == obj.ID)"][0]["Body"] == "This is an email body" @pytest.fixture() def expected_incident(): with open("test_data/expected_incident") as emails_json: mocked_emails = json.load(emails_json) return mocked_emails @pytest.fixture() def emails_data_as_html(): return emails_data_as_html_including_body() def emails_data_as_html_including_body(): with open("test_data/emails_data_html") as emails_json: mocked_emails = json.load(emails_json) return mocked_emails @pytest.fixture() def emails_data_as_text(): return emails_data_as_text_including_body() def emails_data_as_text_including_body(): with open("test_data/emails_data_text") as emails_json: mocked_emails = json.load(emails_json) return mocked_emails def emails_data_as_html_without_body(): with open("test_data/emails_data_html_without_body") as emails_json: mocked_emails = json.load(emails_json) return mocked_emails def emails_data_as_text_without_body(): with open("test_data/emails_data_text_without_body") as emails_json: mocked_emails = json.load(emails_json) return mocked_emails @pytest.fixture() def emails_data_full_body_as_html(): with open("test_data/emails_data_full_body_html") as emails_json: return json.load(emails_json) @pytest.fixture() def emails_data_full_body_as_text(): with open("test_data/emails_data_full_body_text") as emails_json: return json.load(emails_json) @pytest.fixture() def expected_incident_full_body(): with open("test_data/expected_incident_full_body") as incident: return json.load(incident) @pytest.fixture def last_run_data(): last_run = { "LAST_RUN_TIME": "2019-11-12T15:00:00Z", "LAST_RUN_IDS": [], "LAST_RUN_FOLDER_ID": "last_run_dummy_folder_id", "LAST_RUN_FOLDER_PATH": "Phishing", "LAST_RUN_ACCOUNT": "dummy@mailbox.com", } return last_run @pytest.mark.parametrize( "client, email_content_html, email_content_text", [ (oproxy_client(), emails_data_as_html_including_body(), emails_data_as_text_including_body()), (self_deployed_client(), emails_data_as_html_without_body(), emails_data_as_text_without_body()), ], ) def test_fetch_incidents(client, email_content_html, email_content_text, mocker, last_run_data, expected_incident): """ Given - Case A: emails as text and html including the full body key in the api response. - Case B: emails as text and html without the full body key in the api response. When - fetching incidents when there is a body key and when there isn't a body key. Then - Case A: make sure the 'body' key is being taken even when 'uniqueBody' key exists. - Case B: make sure the 'uniqueBody' is being taken instead of the 'body' key. """ mocker.patch( "CommonServerPython.get_current_time", return_value=dateparser.parse("2019-11-12T15:01:00", settings={"TIMEZONE": "UTC"}) ) # the third argument in side effect is for attachments (no-attachments here) mocker.patch.object(client, "http_request", side_effect=[email_content_html, email_content_text, {}]) mocker.patch.object(demisto, "info") result_next_run, result_incidents = client.fetch_incidents(last_run_data) assert result_next_run.get("time") == "2019-11-12T15:00:30Z" assert result_next_run.get("LAST_RUN_IDS") == ["dummy_id_1"] assert result_next_run.get("LAST_RUN_FOLDER_ID") == "last_run_dummy_folder_id" assert result_next_run.get("LAST_RUN_FOLDER_PATH") == "Phishing" result_incidents = result_incidents[0] result_raw_json = json.loads(result_incidents.pop("rawJSON")) expected_raw_json = expected_incident.pop("rawJSON", None) assert result_raw_json == expected_raw_json assert result_incidents == expected_incident class TestFetchIncidentsWithLookBack: FREEZE_TIMESTAMP = "2022-07-28T12:09:17Z" @staticmethod def start_freeze_time(timestamp): from freezegun import freeze_time _start_freeze_time = freeze_time(timestamp) _start_freeze_time.start() return datetime.now() def create_incidents_queue(self): first_email = { "id": "1", "subject": "email-1", "receivedDateTime": (self.start_freeze_time(self.FREEZE_TIMESTAMP) - timedelta(minutes=2)).strftime(API_DATE_FORMAT), } second_email = { "id": "2", "subject": "email-2", "receivedDateTime": (self.start_freeze_time(self.FREEZE_TIMESTAMP) - timedelta(minutes=5)).strftime(API_DATE_FORMAT), } third_email = { "id": "3", "subject": "email-3", "receivedDateTime": (self.start_freeze_time(self.FREEZE_TIMESTAMP) - timedelta(minutes=10)).strftime(API_DATE_FORMAT), } return [([third_email], []), ([second_email, third_email], []), ([first_email, second_email, third_email], [])] @pytest.mark.parametrize("look_back", [30, 40, 400]) def test_fetch_emails_with_look_back_greater_than_zero(self, mocker, look_back): """ Given - a look back parameter. - incidents queue. When - trying to fetch emails with the look-back mechanism. Then - make sure only one incident is being returned each time, based on the 'cache' look-back mechanism. - make sure the correct timestamp to query the api was called based on the look-back parameter. - make sure the correct incident is being returned by its name without any duplication whatsoever. - make sure the 'time' for the look-back for the last run is being set to the latest incident occurred incident - make sure the 'ID' field is being removed from the incidents before fetching. """ client = self_deployed_client() client._look_back = look_back last_emails_mocker = mocker.patch.object(client, "_fetch_last_emails", side_effect=self.create_incidents_queue()) mocker.patch.object(client, "_get_email_attachments", return_value=[]) last_run = { "LAST_RUN_FOLDER_ID": "last_run_dummy_folder_id", "LAST_RUN_FOLDER_PATH": "Phishing", "LAST_RUN_ACCOUNT": "dummy@mailbox.com", "LAST_RUN_TIME": (datetime.now() - timedelta(minutes=20)).strftime(API_DATE_FORMAT), } expected_last_run_timestamps = ["2022-07-28T12:07:17Z", "2022-07-28T12:04:17Z", "2022-07-28T11:59:17Z"] for i in range(3, 0, -1): next_run, incidents = client.fetch_incidents(last_run=last_run) assert last_emails_mocker.call_args.kwargs["last_fetch"] == (datetime.now() - timedelta(minutes=look_back)).strftime( API_DATE_FORMAT ) assert next_run["time"] == expected_last_run_timestamps[i - 1] assert len(incidents) == 1 assert incidents[0]["name"] == f"email-{i}" assert "ID" not in incidents[0] @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_fetch_incidents_changed_folder(mocker, client, emails_data_as_html, emails_data_as_text, last_run_data): changed_folder = "Changed_Folder" client._folder_to_fetch = changed_folder mocker_folder_by_path = mocker.patch.object(client, "_get_folder_by_path", return_value={"id": "some_dummy_folder_id"}) # the third argument in side effect is for attachments (no-attachments here) mocker.patch.object(client, "http_request", side_effect=[emails_data_as_html, emails_data_as_text, {}]) mocker.patch.object(demisto, "info") client.fetch_incidents(last_run_data) mocker_folder_by_path.assert_called_once_with("dummy@mailbox.com", changed_folder, overwrite_rate_limit_retry=True) @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_fetch_incidents_changed_account(mocker, client, emails_data_as_html, emails_data_as_text, last_run_data): changed_account = "Changed_Account" client._mailbox_to_fetch = changed_account mocker_folder_by_path = mocker.patch.object(client, "_get_folder_by_path", return_value={"id": "some_dummy_folder_id"}) # the third argument in side effect is for attachments (no-attachments here) mocker.patch.object(client, "http_request", side_effect=[emails_data_as_html, emails_data_as_text, {}]) mocker.patch.object(demisto, "info") client.fetch_incidents(last_run_data) mocker_folder_by_path.assert_called_once_with( changed_account, last_run_data["LAST_RUN_FOLDER_PATH"], overwrite_rate_limit_retry=True ) @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_fetch_incidents_detect_initial(mocker, client, emails_data_as_html, emails_data_as_text): mocker_folder_by_path = mocker.patch.object(client, "_get_folder_by_path", return_value={"id": "some_dummy_folder_id"}) # the third argument in side effect is for attachments (no-attachments here) mocker.patch.object(client, "http_request", side_effect=[emails_data_as_html, emails_data_as_text, {}]) mocker.patch.object(demisto, "info") client.fetch_incidents({}) mocker_folder_by_path.assert_called_once_with("dummy@mailbox.com", "Phishing", overwrite_rate_limit_retry=True) @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_fetch_incidents_with_full_body( mocker, client, emails_data_full_body_as_html, emails_data_full_body_as_text, expected_incident_full_body, last_run_data ): """ Given - a flag to fetch the entire email body When - fetching incidents Then - Make sure that in the details section, there is the full email body content. """ mocker.patch( "CommonServerPython.get_current_time", return_value=dateparser.parse("2019-11-12T15:01:00", settings={"TIMEZONE": "UTC"}) ) client._display_full_email_body = True # the third argument in side effect is for attachments (no-attachments here) mocker.patch.object(client, "http_request", side_effect=[emails_data_full_body_as_html, emails_data_full_body_as_text, {}]) mocker.patch.object(demisto, "info") result_next_run, result_incidents = client.fetch_incidents(last_run_data) assert result_next_run.get("time") == "2019-11-12T15:00:30Z" assert result_next_run.get("LAST_RUN_IDS") == ["dummy_id_1"] assert result_next_run.get("LAST_RUN_FOLDER_ID") == "last_run_dummy_folder_id" assert result_next_run.get("LAST_RUN_FOLDER_PATH") == "Phishing" result_incidents = result_incidents[0] result_raw_json = json.loads(result_incidents.pop("rawJSON")) expected_raw_json = expected_incident_full_body.pop("rawJSON", None) assert result_raw_json == expected_raw_json assert result_incidents == expected_incident_full_body def test_parse_email_as_label(): assert GraphMailUtils.parse_email_as_labels({"ID": "dummy_id"}) == [{"type": "Email/ID", "value": "dummy_id"}] assert GraphMailUtils.parse_email_as_labels({"To": ["dummy@recipient.com"]}) == [ {"type": "Email/To", "value": "dummy@recipient.com"} ] def test_build_recipient_input(): recipient_input = ["dummy1@rec.com", "dummy2@rec.com", "dummy3@rec.com"] # disable-secrets-detection result_recipients_input = GraphMailUtils.build_recipient_input(recipient_input) expected_recipients_input = [ {"emailAddress": {"address": "dummy1@rec.com"}}, {"emailAddress": {"address": "dummy2@rec.com"}}, {"emailAddress": {"address": "dummy3@rec.com"}}, ] assert result_recipients_input == expected_recipients_input def test_build_body_input(): first_body_input = ["test body 1", "text"] second_body_input = ["test body 2", "HTML"] first_result_body_input = GraphMailUtils.build_body_input(*first_body_input) second_result_body_input = GraphMailUtils.build_body_input(*second_body_input) assert first_result_body_input == {"content": "test body 1", "contentType": "text"} assert second_result_body_input == {"content": "test body 2", "contentType": "HTML"} def test_build_headers_input(): headers_input = ["x-header-one:header1", "x-header-two:heasder2"] result_expecte_headers = [{"name": "x-header-one", "value": "header1"}, {"name": "x-header-two", "value": "heasder2"}] assert GraphMailUtils.build_headers_input(headers_input) == result_expecte_headers def test_build_message(): message_input = { "to_recipients": ["dummy@recipient.com"], # disable-secrets-detection "cc_recipients": ["dummyCC@recipient.com"], # disable-secrets-detection "bcc_recipients": ["dummyBCC@recipient.com"], # disable-secrets-detection "reply_to": ["dummyreplyTo@recipient.com"], # disable-secrets-detection "subject": "Dummy Subject", "body": "Dummy Body", "body_type": "text", "flag": "flagged", "importance": "Normal", "internet_message_headers": None, "attach_ids": [], "attach_names": [], "attach_cids": [], "manual_attachments": [], } expected_message = { "toRecipients": [{"emailAddress": {"address": "dummy@recipient.com"}}], # disable-secrets-detection "ccRecipients": [{"emailAddress": {"address": "dummyCC@recipient.com"}}], # disable-secrets-detection "bccRecipients": [{"emailAddress": {"address": "dummyBCC@recipient.com"}}], # disable-secrets-detection "replyTo": [{"emailAddress": {"address": "dummyreplyTo@recipient.com"}}], # disable-secrets-detection "subject": "Dummy Subject", "body": {"content": "Dummy Body", "contentType": "text"}, "bodyPreview": "Dummy Body", "importance": "Normal", "flag": {"flagStatus": "flagged"}, "attachments": [], } result_message = GraphMailUtils.build_message(**message_input) assert result_message == expected_message @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_get_attachment_as_command_result(client): """ Given: - raw response returned from get_attachment_command When: - response type is itemAttachment and 'item_result_creator' is called - The 'should_download_message_attachment' command argument value is False (by default) Then: - Validate that the message object created successfully - GraphMailUtils.item_result_creator function should return a command result """ output_prefix = "MSGraphMail(val.ID && val.ID == obj.ID)" with open("test_data/mail_with_attachment") as mail_json: user_id = "ex@example.com" raw_response = json.load(mail_json) args = {} res = GraphMailUtils.item_result_creator(raw_response, user_id, args, client) assert isinstance(res, CommandResults) output = res.to_context().get("EntryContext", {}) assert output.get(output_prefix).get("ID") == "exampleID" assert output.get(output_prefix).get("Subject") == "Test it" @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_get_attachment_as_file_result(mocker, client): """ Given: - raw response returned from get_attachment_command When: - response type is itemAttachment and 'item_result_creator' is called. - The 'should_download_message_attachment' command argument is True Then: - Validate that the message object created successfully - GraphMailUtils.item_result_creator function should return a command result """ mocker.patch.object(MsGraphMailBaseClient, "_get_attachment_mime", return_value="raw data") with open("test_data/mail_with_attachment") as mail_json: user_id = "ex@example.com" args = { "message_id": "example_message_id", "attachment_id": "example_attachment_id", "should_download_message_attachment": True, } raw_response = json.load(mail_json) res = GraphMailUtils.item_result_creator(raw_response, user_id, args, client) assert isinstance(res, dict) assert res["File"] == "Test_it.eml" assert res["FileID"] @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_get_attachments_without_attachment_id(mocker, client): """ Given: - A user ID 'ex@example.com' When: - Calling 'get_attachment_command' method. Then: - Validate that the message object created successfully and all the attachment where downloaded. """ from MicrosoftGraphMail import get_attachment_command file_attachments_result = {"2": "f1145f66-90fe-4604-a7ea-faac8c33684e-attachmentName-image2.png", "3": "image3.png"} output_prefix = "MSGraphMail(val.ID && val.ID == obj.ID)" with open("test_data/mail_with_attachments") as mail_json: user_id = "ex@example.com" test_args = {"user_id": user_id} raw_response = json.load(mail_json) mocker.patch.object(client, "get_attachment", return_value=raw_response) res = get_attachment_command(client, test_args) assert isinstance(res, List) assert len(res) == len(raw_response) for i, attachment in enumerate(res): if isinstance(attachment, CommandResults): output = attachment.to_context().get("EntryContext", {}) assert output.get(output_prefix).get("ID") == f"exampleID{i}" assert output.get(output_prefix).get("Subject") == f"Test it{i}" else: assert attachment["File"] == file_attachments_result.get(str(i)) @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_get_attachment_unsupported_type(client): """ Given: - raw response returned from get_attachment_command When: - response type is itemAttachment with attachment that is not supported Then: - Validate the human readable which explain we do not support the type """ with open("test_data/mail_with_unsupported_attachment") as mail_json: user_id = "ex@example.com" raw_response = json.load(mail_json) args = {} res = GraphMailUtils.item_result_creator(raw_response, user_id, args, client) assert isinstance(res, CommandResults) output = res.to_context().get("HumanReadable", "") assert "Integration does not support attachments from type #microsoft.graph.contact" in output @pytest.mark.parametrize( "function_name, attachment_type, client", [("file_result_creator", "fileAttachment", oproxy_client()), ("item_result_creator", "itemAttachment", oproxy_client())], ) def test_create_attachment(mocker, function_name, attachment_type, client): """ Given: - raw response returned from api: 1. @odata.type is fileAttachment 2. @odata.type is itemAttachment When: - create_attachment checks the attachment type and decide which function will handle the response Then: - item_result_creator and file_result_creator called respectively to the type """ mocker.patch(f"MicrosoftGraphMail.GraphMailUtils.{function_name}", return_value=function_name) raw_response = {"@odata.type": f"#microsoft.graph.{attachment_type}"} user_id = "ex@example.com" args = {} called_function = GraphMailUtils.create_attachment(raw_response, user_id, args, client) assert called_function == function_name @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_list_attachments_with_name(mocker, client): """ Given: - list attachments command - all attachments has a name When: - parsing email attachments Then: - Validate that the attachments are being parsed correctly """ output_prefix = "MSGraphMailAttachment(val.ID && val.ID == obj.ID)" with open("test_data/list_attachment_result.json") as attachment_result: args = {"user_id": "example"} raw_response = json.load(attachment_result) mocker.patch.object(client, "list_attachments", return_value=raw_response) mocker.patch.object(demisto, "results") command_result = list_attachments_command(client, args) context = command_result.to_context().get("EntryContext") assert context.get(output_prefix).get("Attachment")[0].get("ID") == "someID" assert context.get(output_prefix).get("Attachment")[0].get("Name") == "someName" assert context.get(output_prefix).get("Attachment")[0].get("Type") == "application/octet-stream" @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_list_attachments_without_name(mocker, client): """ Given: - list attachments command - there is an attachment without a name When: - parsing email attachments Then: - Validate that the attachments are being parsed correctly and the name is equal to the ID """ output_prefix = "MSGraphMailAttachment(val.ID && val.ID == obj.ID)" with open("test_data/list_attachment_result_no_name.json") as attachment_result: args = {"user_id": "example"} raw_response = json.load(attachment_result) mocker.patch.object(client, "list_attachments", return_value=raw_response) mocker.patch.object(demisto, "results") command_result = list_attachments_command(client, args) context = command_result.to_context().get("EntryContext") assert context.get(output_prefix).get("Attachment")[0].get("ID") == "someID" assert context.get(output_prefix).get("Attachment")[0].get("Name") == "someID" assert context.get(output_prefix).get("Attachment")[0].get("Type") == "application/octet-stream" @pytest.mark.parametrize("client", [oproxy_client(), self_deployed_client()]) def test_reply_mail_command(client, mocker): """ Given: - reply-mail arguments When: - send a reply mail message Then: - validates that the outputs fit the updated reply mail message """ args = { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "inReplyTo": "id", "from": "ex1@example.com", "replyTo": ["ex2@example.com"], } mocker.patch.object(client, "http_request") reply_message = reply_email_command(client, args) assert reply_message.outputs_prefix == "MicrosoftGraph.SentMail" assert reply_message.outputs_key_field == "ID" assert reply_message.outputs["ID"] == args["inReplyTo"] assert reply_message.outputs["subject"] == "Re: " + args["subject"] assert reply_message.outputs["toRecipients"] == args["to"] assert reply_message.outputs["bodyPreview"] == args["body"] assert reply_message.outputs["replyTo"] == args["replyTo"] SEND_MAIL_COMMAND_ARGS = [ ( oproxy_client(), { "to": ["ex@example.com"], "htmlBody": "<b>This text is bold</b>", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", }, ), ( self_deployed_client(), { "to": ["ex@example.com"], "htmlBody": "<b>This text is bold</b>", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", }, ), ( oproxy_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", }, ), ( self_deployed_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", }, ), ] @pytest.mark.parametrize("client, args", SEND_MAIL_COMMAND_ARGS) def test_send_mail_command(mocker, client, args): """ Given: - send-mail command's arguments When: - sending a mail Then: - validates that http request to send-mail was called with the correct values. """ with requests_mock.Mocker() as request_mocker: from_email = args.get("from") mocker.patch.object(client, "get_access_token") send_mail_mocker = request_mocker.post(f"https://graph.microsoft.com/v1.0/users/{from_email}/SendMail") send_email_command(client, args) assert send_mail_mocker.called message = send_mail_mocker.last_request.json().get("message") assert message assert message.get("toRecipients")[0].get("emailAddress").get("address") == args.get("to")[0] assert message.get("body").get("content") == args.get("htmlBody") or args.get("body") assert message.get("subject") == args.get("subject") assert message.get("replyTo")[0].get("emailAddress").get("address") == args.get("replyTo")[0] assert message.get("replyTo")[1].get("emailAddress").get("address") == args.get("replyTo")[1] class TestCommandsWithLargeAttachments: SEND_MAIL_WITH_LARGE_ATTACHMENTS_COMMAND_ARGS = [ ( self_deployed_client(), { "to": ["ex@example.com"], "htmlBody": "<b>This text is bold</b>", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", "attachIDs": "1", }, ), ( oproxy_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", "attachIDs": "2", }, ), ( self_deployed_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "replyTo": ["ex2@example.com", "ex3@example.com"], "from": "ex1@example.com", "attachIDs": "1,2", }, ), ] REPLY_MAIL_WITH_LARGE_ATTACHMENTS_COMMAND_ARGS = [ ( self_deployed_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "inReplyTo": "123", "from": "ex1@example.com", "attachIDs": "3", }, ), ( oproxy_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "inReplyTo": "123", "from": "ex1@example.com", "attachIDs": "4", }, ), ( self_deployed_client(), { "to": ["ex@example.com"], "body": "test body", "subject": "test subject", "inReplyTo": "123", "from": "ex1@example.com", "attachIDs": "3,4", }, ), ] @staticmethod def expected_upload_headers(world_file=True): if world_file: for header in [ # testing on the world.jpg file. { "Content-Length": "3145728", "Content-Range": "bytes 0-3145727/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 3145728-6291455/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 6291456-9437183/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 9437184-12582911/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 12582912-15728639/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 15728640-18874367/21796912", "Content-Type": "application/octet-stream", }, { "Content-Length": "2922544", "Content-Range": "bytes 18874368-21796911/21796912", "Content-Type": "application/octet-stream", }, ]: yield header else: for header in [ # testing on the test.pdf { "Content-Length": "3145728", "Content-Range": "bytes 0-3145727/4512758", "Content-Type": "application/octet-stream", }, { "Content-Length": "1367030", "Content-Range": "bytes 3145728-4512757/4512758", "Content-Type": "application/octet-stream", }, { "Content-Length": "3145728", "Content-Range": "bytes 6291456-9437183/10520433", "Content-Type": "application/octet-stream", }, { "Content-Length": "1083249", "Content-Range": "bytes 9437184-10520432/10520433", "Content-Type": "application/octet-stream", }, ]: yield header @staticmethod def get_attachment_file_details_by_attachment_id(attach_id): attachment_info = { "1": { "path": "test_data/world.jpg", # bigger than 3mb attachment "name": "world.jpg", }, "2": { "path": "test_data/plant.jpg", # smaller than 3mb attachment "name": "plant.jpg", }, "3": { "path": "test_data/test.pdf", # bigger than 3mb attachment "name": "test.pdf", }, "4": { "path": "test_data/sample.pdf", # smaller than 3mb attachment "name": "sample-pdf", }, } return attachment_info.get(attach_id) @staticmethod def upload_response_side_effect(**kwargs): headers = kwargs.get("headers") if int(headers["Content-Length"]) < MsGraphMailClient.MAX_ATTACHMENT_SIZE: return MockedResponse(status_code=201) return MockedResponse(status_code=200) def validate_upload_attachments_flow(self, create_upload_mock, upload_query_mock, world_file=True): """ Validates that the upload flow is working as expected, each piece of headers is sent as expected. """ if not create_upload_mock.called: return False if create_upload_mock.call_count != 1: return False expected_headers = iter(self.expected_upload_headers(world_file=world_file)) for i in range(upload_query_mock.call_count): current_headers = next(expected_headers) if upload_query_mock.mock_calls[i].kwargs["headers"] != current_headers: return False return True @pytest.mark.parametrize("client, args", SEND_MAIL_WITH_LARGE_ATTACHMENTS_COMMAND_ARGS) def test_send_mail_command(self, mocker, client, args): """ Given: Case 1: send email command arguments and attachment > 3mb. Case 2: send email command arguments and attachment < 3mb. Case 3: send email command arguments and one attachment > 3m and one attachment < 3mb. When: - sending a mail Then: Case1: * make sure an upload session was created and that the correct headers were sent * make sure the endpoint to send an email without creating draft mail was not called. * make sure the endpoint to create a draft mail and send a draft mail were called. Case2: * make sure an upload session was not created * make sure the endpoints to create a draft email and send the draft email were not called. * make sure the endpoint to send an email was called. Case3: * make sure an upload session was created and that the correct headers were sent. * make sure the endpoint to send an email without creating draft mail was not called. * make sure the endpoint to create a draft email and send the draft mail were called. * make sure the the attachment < 3mb was sent when creating a draft mail not through an upload session. - Make sure for all three cases the expected context output is returned. """ with requests_mock.Mocker() as request_mocker: from_email = args.get("from") mocked_draft_id = "123" mocker.patch.object(client, "get_access_token") mocker.patch.object(demisto, "getFilePath", side_effect=self.get_attachment_file_details_by_attachment_id) create_draft_mail_mocker = request_mocker.post( f"https://graph.microsoft.com/v1.0/users/{from_email}/messages", json={"id": mocked_draft_id} ) send_draft_mail_mocker = request_mocker.post( # mock the endpoint to send a draft mail f"https://graph.microsoft.com/v1.0/users/{from_email}/messages/{mocked_draft_id}/send" ) send_mail_mocker = request_mocker.post(f"https://graph.microsoft.com/v1.0/users/{from_email}/SendMail") create_upload_mock = mocker.patch.object(client, "get_upload_session", return_value={"uploadUrl": "test.com"}) upload_query_mock = mocker.patch.object(requests, "put", side_effect=self.upload_response_side_effect) send_email_command(client, args) # attachment 1 is an attachment bigger than 3MB if "1" in args.get("attachIDs"): # means the attachment should be created in the upload session assert create_draft_mail_mocker.called assert send_draft_mail_mocker.called assert not send_mail_mocker.called assert self.validate_upload_attachments_flow(create_upload_mock, upload_query_mock) if "2" in args.get("attachIDs"): assert create_draft_mail_mocker.last_request.json().get("attachments") draft_sent_json = create_draft_mail_mocker.last_request.json() assert draft_sent_json assert draft_sent_json.get("toRecipients") assert draft_sent_json.get("subject") else: assert not create_draft_mail_mocker.called assert not send_draft_mail_mocker.called assert send_mail_mocker.called message = send_mail_mocker.last_request.json().get("message") assert message assert message.get("toRecipients")[0].get("emailAddress").get("address") == args.get("to")[0] assert message.get("body").get("content") == args.get("htmlBody") or args.get("body") assert message.get("subject") == args.get("subject") assert message.get("replyTo")[0].get("emailAddress").get("address") == args.get("replyTo")[0] assert message.get("replyTo")[1].get("emailAddress").get("address") == args.get("replyTo")[1] assert message.get("attachments") @pytest.mark.parametrize("client, args", REPLY_MAIL_WITH_LARGE_ATTACHMENTS_COMMAND_ARGS) def test_reply_mail_command(self, mocker, client, args): """ Given: Case 1: reply email command arguments and attachment > 3mb. Case 2: reply email command arguments and attachment < 3mb. Case 3: reply email command arguments and one attachment > 3m and one attachment < 3mb. When: - sending a reply mail Then: Case1: * make sure an upload session was created and that the correct headers were sent * make sure the endpoint to send a reply without creating draft mail was not called. * make sure the endpoint to create a draft reply mail and send a reply draft mail were called. Case2: * make sure an upload session was not created * make sure the endpoints to create a draft reply and send a draft reply were not called. Case3: * make sure an upload session was created and that the correct headers were sent. * make sure the endpoint to send a reply without creating draft mail was not called. * make sure the endpoint to create a draft reply mail and send a reply draft mail were called. * make sure the the attachment < 3mb was sent when creating a draft reply not through an upload session. - Make sure for all three cases the expected context output is returned. """ with requests_mock.Mocker() as request_mocker: from_email = args.get("from") mocked_draft_id = "123" reply_message_id = args.get("inReplyTo") mocker.patch.object(client, "get_access_token") mocker.patch.object(demisto, "getFilePath", side_effect=self.get_attachment_file_details_by_attachment_id) create_draft_mail_mocker = request_mocker.post( # mock the endpoint to create a draft for an existing message f"https://graph.microsoft.com/v1.0/users/{from_email}/messages/{reply_message_id}/createReply", json={"id": mocked_draft_id}, ) send_reply_draft_mail_mocker = request_mocker.post( # mock the endpoint to reply a draft mail f"https://graph.microsoft.com/v1.0/users/{from_email}/messages/{mocked_draft_id}/send" ) create_upload_mock = mocker.patch.object(client, "get_upload_session", return_value={"uploadUrl": "test.com"}) upload_query_mock = mocker.patch.object(requests, "put", side_effect=self.upload_response_side_effect) reply_mail_mocker = request_mocker.post( f"https://graph.microsoft.com/v1.0/users/{from_email}/messages/{reply_message_id}/reply" ) command_results = reply_email_command(client, args) if "3" in args.get("attachIDs"): assert create_draft_mail_mocker.called assert send_reply_draft_mail_mocker.called # sending the draft reply email should be called assert not reply_mail_mocker.called assert self.validate_upload_attachments_flow(create_upload_mock, upload_query_mock, world_file=False) if "4" in args.get("attachIDs"): # make sure when creating draft to send a reply that the attachments are being added to the api # call the create a draft and not through upload session assert create_draft_mail_mocker.last_request.json().get("message").get("attachments") else: assert reply_mail_mocker.called assert not create_draft_mail_mocker.called assert not create_upload_mock.called assert not send_reply_draft_mail_mocker.called assert command_results.outputs == { "toRecipients": ["ex@example.com"], "subject": "Re: test subject", "bodyPreview": "test body", "ID": "123", } @pytest.mark.parametrize("client, args", SEND_MAIL_WITH_LARGE_ATTACHMENTS_COMMAND_ARGS) def test_create_draft_email_command(self, mocker, client, args): """ Given: Case 1: create draft command arguments and attachment > 3mb. Case 2: create draft command arguments and attachment < 3mb. Case 3: create draft command arguments and one attachment > 3m and one attachment < 3mb. When: creating a draft mail. Then: Case1: * make sure an upload session was created and that the correct headers were sent * make sure the endpoint to create a draft email was called. Case2: * make sure an upload session was not created * make sure the endpoint to create a draft mail was called with the attachment. Case3: * make sure an upload session was created and that the correct headers were sent. * make sure the endpoint to create a draft mail was called. * make sure the the attachment < 3mb was sent when creating a draft reply not through an upload session - Make sure for all three cases the expected context output is returned. """ from MicrosoftGraphMail import create_draft_command with requests_mock.Mocker() as request_mocker: from_email = args.get("from") mocker.patch.object(client, "get_access_token") create_draft_mail_mocker = request_mocker.post( f"https://graph.microsoft.com/v1.0/users/{from_email}/messages", json={"id": "123"} ) mocker.patch.object(demisto, "getFilePath", side_effect=self.get_attachment_file_details_by_attachment_id) create_upload_mock = mocker.patch.object(client, "get_upload_session", return_value={"uploadUrl": "test.com"}) upload_query_mock = mocker.patch.object(requests, "put", side_effect=self.upload_response_side_effect) command_result = create_draft_command(client, args) # attachment 1 is an attachment bigger than 3MB if "1" in args.get("attachIDs"): # means the attachment should be created in the upload session assert create_upload_mock.called assert upload_query_mock.called assert self.validate_upload_attachments_flow(create_upload_mock, upload_query_mock) if "2" in args.get("attachIDs"): assert create_draft_mail_mocker.last_request.json()["attachments"] else: assert not create_upload_mock.called assert not upload_query_mock.called assert create_draft_mail_mocker.last_request.json()["attachments"] assert command_result.outputs["ID"] == "123" assert create_draft_mail_mocker.called assert create_draft_mail_mocker.last_request.json() @pytest.mark.parametrize( "server_url, expected_endpoint", [ ("https://graph.microsoft.us", "gcc-high"), ("https://dod-graph.microsoft.us", "dod"), ("https://graph.microsoft.de", "de"), ("https://microsoftgraph.chinacloudapi.cn", "cn"), ], ) def test_server_to_endpoint(server_url, expected_endpoint): """ Given: - Host address for national endpoints When: - Creating a new MsGraphMailClient Then: - Verify that the host address is translated to the correct endpoint code, i.e. com/gcc-high/dod/de/cn """ from MicrosoftApiModule import GRAPH_BASE_ENDPOINTS assert GRAPH_BASE_ENDPOINTS[server_url] == expected_endpoint def test_fetch_last_emails__with_exclude(mocker): """ Given: - Last fetch fetched until email 2 - Next fetch will fetch 5 emails - Exclusion list contains 2/5 emails ids - fetch limit is set to 2 When: - Calling fetch_incidents Then: - Fetch 2 emails - Save previous 2 fetched mails + 2 new mails - Fetch emails after exclude id - Don't fetch emails after limit """ emails = { "value": [ {"receivedDateTime": "1", "id": "1"}, {"receivedDateTime": "2", "id": "2"}, {"receivedDateTime": "4", "id": "3"}, {"receivedDateTime": "4", "id": "4"}, {"receivedDateTime": "4", "id": "5"}, ] } client = oproxy_client() client._emails_fetch_limit = 2 mocker.patch.object(client, "http_request", return_value=emails) fetched_emails, ids = client._fetch_last_emails("", last_fetch="2022-07-28T12:09:17Z", exclude_ids=["1", "2"]) assert len(fetched_emails) == 2 assert ids == ["3", "4"] assert fetched_emails[0] == emails["value"][2] assert fetched_emails[1] == emails["value"][3] def test_fetch_last_emails__no_exclude(mocker): """ Given: - No previous last fetch - Next fetch will fetch 1 email - fetch limit is set to 2 When: - Calling fetch_incidents Then: - Fetch 1 email - Save mail in exclusion """ emails = { "value": [ {"receivedDateTime": "1", "id": "1"}, ] } client = oproxy_client() client._emails_fetch_limit = 2 mocker.patch.object(client, "http_request", return_value=emails) fetched_emails, ids = client._fetch_last_emails("", last_fetch="2022-07-28T12:09:17Z", exclude_ids=[]) assert len(fetched_emails) == 1 assert ids == ["1"] assert fetched_emails[0] == emails["value"][0] def test_fetch_last_emails__all_mails_in_exclude(mocker): """ Given: - Last fetch fetched until email 2 - Next fetch will fetch 2 emails - Exclusion list contains 2/2 emails ids When: - Calling fetch_incidents Then: - Fetch 0 emails - Save previous 2 fetched mails """ emails = { "value": [ {"receivedDateTime": "1", "id": "1"}, {"receivedDateTime": "2", "id": "2"}, ] } client = oproxy_client() client._emails_fetch_limit = 2 mocker.patch.object(client, "http_request", return_value=emails) fetched_emails, ids = client._fetch_last_emails("", last_fetch="2022-07-28T12:09:17Z", exclude_ids=["1", "2"]) assert len(fetched_emails) == 0 assert ids == ["1", "2"] @pytest.mark.parametrize( "args", [ ({"user_id": "test@mail.com", "message_ids": "EMAIL1", "status": "Read"}), ({"user_id": "test@mail.com", "message_ids": "EMAIL1", "folder_id": "Inbox", "status": "Read"}), ({"user_id": "test@mail.com", "message_ids": "EMAIL1", "status": "Unread"}), ({"user_id": "test@mail.com", "message_ids": "EMAIL1", "folder_id": "Inbox", "status": "Unread"}), ], ) def test_update_email_status_command(mocker, args: dict): import MicrosoftGraphMail client = self_deployed_client() mocker.patch.object(client, "http_request") result = MicrosoftGraphMail.update_email_status_command(client=client, args=args) mark_as_read = args["status"].lower() == "read" folder_id = args.get("folder_id") folder_path = f"/{GraphMailUtils.build_folders_path(folder_id)}" if folder_id else "" url_suffix = f"/users/{args['user_id']}{folder_path}/messages/{args['message_ids']}" assert result.outputs is None client.http_request.assert_called_with(method="PATCH", url_suffix=url_suffix, json_data={"isRead": mark_as_read}) @pytest.mark.parametrize(argnames="client_id", argvalues=["test_client_id", None]) def test_test_module_command_with_managed_identities(mocker, requests_mock, client_id): """ Given: - Managed Identities client id for authentication. When: - Calling test_module. Then: - Ensure the output are as expected. """ import re from MicrosoftGraphMail import MANAGED_IDENTITIES_TOKEN_URL, Resources, main mock_token = {"access_token": "test_token", "expires_in": "86400"} get_mock = requests_mock.get(MANAGED_IDENTITIES_TOKEN_URL, json=mock_token) requests_mock.get(re.compile(f"^{Resources.graph}.*"), json={}) params = {"managed_identities_client_id": {"password": client_id}, "use_managed_identities": "True"} mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "results", return_value=params) mocker.patch("MicrosoftApiModule.get_integration_context", return_value={}) main() assert "ok" in demisto.results.call_args[0][0] qs = get_mock.last_request.qs assert qs["resource"] == [Resources.graph] assert (client_id and qs["client_id"] == [client_id]) or "client_id" not in qs @pytest.mark.parametrize( "raw_attachment, legacy_name, expected_name, expect_exception", [ ( { "name": "test.png", "contentId": "123", "isInline": True, "contentBytes": base64.b64encode(b"test data").decode("utf-8"), }, False, "123-attachmentName-test.png", False, ), ( { "name": "test.png", "contentId": None, "isInline": False, "contentBytes": base64.b64encode(b"test data").decode("utf-8"), }, False, "test.png", False, ), ( { "name": "test.png", "contentId": "123", "isInline": True, "contentBytes": base64.b64encode(b"test data").decode("utf-8"), }, True, "test.png", False, ), ( { "name": "test.png", "contentId": "None", "isInline": True, "contentBytes": base64.b64encode(b"test data").decode("utf-8"), }, False, "test.png", False, ), ({"name": "test.png", "contentId": "123", "isInline": True, "contentBytes": "invalid_base64"}, False, None, True), ], ) def test_file_result_creator(monkeypatch, raw_attachment, legacy_name, expected_name, expect_exception): from MicrosoftGraphMailApiModule import GraphMailUtils monkeypatch.setattr("MicrosoftGraphMail.fileResult", fileResult) monkeypatch.setattr("MicrosoftGraphMail.DemistoException", DemistoException) if expect_exception: with pytest.raises(DemistoException): GraphMailUtils.file_result_creator(raw_attachment, legacy_name) else: result = GraphMailUtils.file_result_creator(raw_attachment, legacy_name) assert result["File"] == expected_name def test_add_attachment_with_upload_session__404_once_then_success_and_final_201(mocker): """ Given: - MsGraphMail client with MAX_ATTACHMENT_SIZE=4. - get_upload_session() succeeds and returns a valid uploadUrl. - upload_attachment() behavior per chunk: * Chunk (0,4): first attempt 404, retry succeeds with 200 (mid-chunk). * Chunk (4,8): 200 (mid-chunk). * Chunk (8,10): 201 (final). - A 10-byte attachment ("abcdefghij") producing three chunks: [0,4), [4,8), [8,10). When: - add_attachment_with_upload_session() is invoked to upload the attachment via an upload session. Then: - The first chunk is retried exactly once due to a 404, then proceeds. - The second chunk uploads once with 200 (mid-chunk). - The final chunk returns 201, ending the loop successfully. - Calls per chunk: (0,4) == 2, (4,8) == 1, (8,10) == 1. """ client = self_deployed_client() # Make chunking deterministic: 10 bytes => [0,4), [4,8), [8,10) client.MAX_ATTACHMENT_SIZE = 4 # Upload session: succeed immediately with an upload URL client.get_upload_session = lambda **kw: {"uploadUrl": "https://example/upload"} # Track call counts per (start, endExclusive) calls = {} def upload_attachment(self, upload_url, start_chunk_idx, end_chunk_idx, chunk_data, attachment_size): key = (start_chunk_idx, end_chunk_idx) calls[key] = calls.get(key, 0) + 1 # First chunk: first attempt 404, then 200 if key == (0, 4): return MockedResponse(404) if calls[key] == 1 else MockedResponse(200) # Second chunk: mid-chunk OK if key == (4, 8): return MockedResponse(200) # Final chunk: done if key == (8, 10): return MockedResponse(201) raise AssertionError(f"Unexpected chunk {key}") mocker.patch.object(client, "upload_attachment", types.MethodType(upload_attachment, client)) # Act client.add_attachment_with_upload_session( email="x@y", draft_id="d1", attachment_data=b"abcdefghij", # len=10 attachment_name="f.txt", is_inline=False, ) # Assert: first chunk retried exactly once; others once assert calls[(0, 4)] == 2 assert calls[(4, 8)] == 1 assert calls[(8, 10)] == 1 def test_upload_chunk_404_twice_raises(mocker): """ Given: - MsGraphMail client with MAX_ATTACHMENT_SIZE=4 and a valid uploadUrl. - upload_attachment() for the first chunk (0,4) returns 404 twice. When: - add_attachment_with_upload_session() is called for a 10-byte attachment (three chunks total). Then: - The function retries the failing chunk once. - On the second 404 for the same chunk, it raises DemistoException. - The exception message contains '404' and mentions the failing 'range'. """ client = self_deployed_client() client.MAX_ATTACHMENT_SIZE = 4 mocker.patch.object(client, "get_upload_session", lambda **kw: {"uploadUrl": "https://example/upload"}) attempts = {"n": 0} def upload_attachment(self, upload_url, start_chunk_idx, end_chunk_idx, chunk_data, attachment_size): # Always fail on the first chunk to trigger the double-404 path assert (start_chunk_idx, end_chunk_idx) == (0, 4) attempts["n"] += 1 # Provide .text on the second response so error formatting can include details if json() is absent if attempts["n"] == 2: r = MockedResponse(404) r.text = '{"error":{"code":"ErrorItemNotFound"}}' return r return MockedResponse(404) mocker.patch.object(client, "upload_attachment", types.MethodType(upload_attachment, client)) mocker.patch.object(demisto, "debug", lambda *a, **k: None) with pytest.raises(DemistoException) as ei: client.add_attachment_with_upload_session( email="x@y", draft_id="d1", attachment_data=b"abcdefghij", # len=10 attachment_name="f.txt", is_inline=False, ) msg = str(ei.value) # We at least surface it's a 404 failure; range text may be inclusive in your log/exception formatting. assert "404" in msg assert "range" in msg def test_get_upload_session_retried_on_notfound(mocker): """ Given: - MsGraphMail client with MAX_ATTACHMENT_SIZE=4. - get_upload_session() raises NotFoundError on the first call and succeeds on the second. - upload_attachment() for the single-chunk file returns 201 (final). When: - add_attachment_with_upload_session() is called for a 4-byte attachment (exactly one chunk). Then: - get_upload_session() is attempted at least twice (one failure + one success). - The upload proceeds and completes successfully with a 201 final response. """ client = self_deployed_client() client.MAX_ATTACHMENT_SIZE = 4 attempts = {"n": 0} def get_upload_session(**kw): attempts["n"] += 1 if attempts["n"] == 1: raise NotFoundError("transient not found") return {"uploadUrl": "https://example/upload"} # One-chunk upload that ends with 201 def upload_attachment(self, **kw): return MockedResponse(201) mocker.patch.object(client, "get_upload_session", get_upload_session) mocker.patch.object(client, "upload_attachment", types.MethodType(upload_attachment, client)) mocker.patch.object(demisto, "debug", lambda *a, **k: None) client.add_attachment_with_upload_session( email="x@y", draft_id="d1", attachment_data=b"abcd", # exactly one chunk attachment_name="f.txt", is_inline=False, ) assert attempts["n"] == 2 def test_build_inline_layout_attachments_small_file(mocker): """ Given: - An inline layout attachment whose data is under 3MB (100 bytes). When: - _build_inline_layout_attachments_input is called. Then: - The result dict contains @odata.type, contentBytes (base64), isInline, name, contentId, size. - The result dict does NOT contain requires_upload or data keys. """ small_data = b"x" * 100 attachment_input = { "data": small_data, "maintype": "image", "subtype": "png", "name": "image0.png", "cid": "image0.png@abc_def", } result = MsGraphMailBaseClient._build_inline_layout_attachments_input([attachment_input]) assert len(result) == 1 att = result[0] assert att["@odata.type"] == "#microsoft.graph.fileAttachment" assert att["contentBytes"] == base64.b64encode(small_data).decode("utf-8") assert att["isInline"] is True assert att["name"] == "image0.png" assert att["contentId"] == "image0.png@abc_def" assert att["size"] == 100 assert "requires_upload" not in att assert "data" not in att def test_build_inline_layout_attachments_large_file(mocker): """ Given: - An inline layout attachment whose data is over 3MB (3 * 1024 * 1024 + 1 bytes). When: - _build_inline_layout_attachments_input is called. Then: - The result dict contains data (raw bytes), isInline, name, contentId, requires_upload, size. - The result dict does NOT contain @odata.type or contentBytes keys. """ large_data = b"x" * (3 * 1024 * 1024 + 1) attachment_input = { "data": large_data, "maintype": "image", "subtype": "png", "name": "image0.png", "cid": "image0.png@abc_def", } result = MsGraphMailBaseClient._build_inline_layout_attachments_input([attachment_input]) assert len(result) == 1 att = result[0] assert att["data"] == large_data assert att["isInline"] is True assert att["name"] == "image0.png" assert att["contentId"] == "image0.png@abc_def" assert att["requires_upload"] is True assert att["size"] == len(large_data) assert "@odata.type" not in att assert "contentBytes" not in att def test_build_inline_layout_attachments_mixed_sizes(mocker): """ Given: - Two inline layout attachments: one small (under 3MB) and one large (over 3MB). When: - _build_inline_layout_attachments_input is called with both. Then: - Both attachments are returned in the result list. - The small attachment uses the contentBytes format (no requires_upload). - The large attachment uses the requires_upload format (no contentBytes). """ small_data = b"x" * 100 large_data = b"x" * (3 * 1024 * 1024 + 1) attachments_input = [ { "data": small_data, "maintype": "image", "subtype": "png", "name": "small.png", "cid": "small.png@abc_def", }, { "data": large_data, "maintype": "image", "subtype": "png", "name": "large.png", "cid": "large.png@abc_def", }, ] result = MsGraphMailBaseClient._build_inline_layout_attachments_input(attachments_input) assert len(result) == 2 # First attachment: small — should have contentBytes format small_att = result[0] assert small_att["@odata.type"] == "#microsoft.graph.fileAttachment" assert small_att["contentBytes"] == base64.b64encode(small_data).decode("utf-8") assert small_att["isInline"] is True assert small_att["name"] == "small.png" assert small_att["size"] == 100 assert "requires_upload" not in small_att assert "data" not in small_att # Second attachment: large — should have requires_upload format large_att = result[1] assert large_att["data"] == large_data assert large_att["isInline"] is True assert large_att["name"] == "large.png" assert large_att["requires_upload"] is True assert large_att["size"] == len(large_data) assert "@odata.type" not in large_att assert "contentBytes" not in large_att def test_send_mail_with_small_inline_uses_direct_send(mocker): """ Given: - send_email_command is called with an htmlBody containing a small embedded base64 image (under 3MB). When: - The command processes the inline attachment through handle_html and build_message. Then: - The code uses the direct send_mail() path, NOT send_mail_with_upload_session_flow(). """ client = self_deployed_client() # Build a small base64 image (100 bytes of data, well under 3MB) small_image_data = b"x" * 100 b64_image = base64.b64encode(small_image_data).decode("utf-8") html_body = f'<html><body><img src="data:image/png;base64,{b64_image}"></body></html>' args = { "to": ["recipient@example.com"], "htmlBody": html_body, "subject": "test inline image", "from": "sender@example.com", } send_mail_mock = mocker.patch.object(client, "send_mail", return_value=None) upload_flow_mock = mocker.patch.object(client, "send_mail_with_upload_session_flow", return_value=None) send_email_command(client, args) send_mail_mock.assert_called_once() upload_flow_mock.assert_not_called() def test_send_mail_with_small_attachment_uses_direct_send(mocker): """ Given: - send_email_command is called with a small file attachment (under 3MB) via attachIDs. When: - The command processes the attachment through build_message. Then: - The code uses the direct send_mail() path, NOT send_mail_with_upload_session_flow(). """ client = self_deployed_client() small_file_data = b"x" * 100 mocker.patch.object( GraphMailUtils, "read_file", return_value=(small_file_data, len(small_file_data), "small_file.txt"), ) args = { "to": ["recipient@example.com"], "body": "test body", "subject": "test small attachment", "from": "sender@example.com", "attachIDs": "attach1", } send_mail_mock = mocker.patch.object(client, "send_mail", return_value=None) upload_flow_mock = mocker.patch.object(client, "send_mail_with_upload_session_flow", return_value=None) send_email_command(client, args) send_mail_mock.assert_called_once() upload_flow_mock.assert_not_called() def test_send_mail_with_large_attachment_uses_upload_session(mocker): """ Given: - send_email_command is called with a large file attachment (>= 3MB) via attachIDs. When: - The command processes the attachment through build_message. Then: - The code uses send_mail_with_upload_session_flow(), NOT the direct send_mail(). """ client = self_deployed_client() large_file_data = b"x" * (3 * 1024 * 1024 + 1) mocker.patch.object( GraphMailUtils, "read_file", return_value=(large_file_data, len(large_file_data), "large_file.bin"), ) args = { "to": ["recipient@example.com"], "body": "test body", "subject": "test large attachment", "from": "sender@example.com", "attachIDs": "attach1", } send_mail_mock = mocker.patch.object(client, "send_mail", return_value=None) upload_flow_mock = mocker.patch.object(client, "send_mail_with_upload_session_flow", return_value=None) send_email_command(client, args) send_mail_mock.assert_not_called() upload_flow_mock.assert_called_once() def test_send_mail_with_large_inline_image_uses_upload_session(mocker): """ Given: - send_email_command is called with an htmlBody containing a large embedded base64 image (>= 3MB). When: - The command processes the inline attachment through handle_html and build_message. Then: - The code uses send_mail_with_upload_session_flow(), NOT the direct send_mail(). """ client = self_deployed_client() # Mock handle_html to return a large inline attachment (> 3MB) without needing a huge base64 string large_data = b"x" * (3 * 1024 * 1024 + 1) fake_inline_attachments = [ { "maintype": "image", "subtype": "png", "data": large_data, "name": "image0.png", "cid": "image0.png@abc_def", } ] mocker.patch.object( GraphMailUtils, "handle_html", return_value=("<html><body><img src='cid:image0.png@abc_def'></body></html>", fake_inline_attachments), ) args = { "to": ["recipient@example.com"], "htmlBody": "<html><body><img src='data:image/png;base64,AAAA'></body></html>", "subject": "test large inline image", "from": "sender@example.com", } send_mail_mock = mocker.patch.object(client, "send_mail", return_value=None) upload_flow_mock = mocker.patch.object(client, "send_mail_with_upload_session_flow", return_value=None) send_email_command(client, args) send_mail_mock.assert_not_called() upload_flow_mock.assert_called_once() def test_create_rule_happy_minimal(mocker): """ Given: - Minimal required args (user_id, display_name, actions, sequence=1). When: - Running create_rule_command and mocking the POST to Graph. Then: - HTTP POST goes to /users/{user_id}/mailFolders/inbox/messageRules. - Body contains displayName, sequence=1 and actions. - CommandResults has the expected prefix/key and @odata.* stripped. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "user@example.com" api_response = { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('u')/mailFolders('inbox')/messageRules/$entity", "@odata.etag": 'W/"abc"', "id": "AQAAAJ1=", "displayName": "minimal rule", "sequence": 1, "isEnabled": True, "actions": {"markAsRead": True}, } args = { "user_id": user_id, "display_name": "minimal rule", "sequence": 1, "actions": '{"markAsRead": true}', } with requests_mock.Mocker() as m: mocked = m.post( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules", json=api_response, ) result = create_rule_command(client, args) assert mocked.call_count == 1 sent_body = mocked.last_request.json() assert sent_body.get("displayName") == "minimal rule" assert sent_body.get("sequence") == 1 assert sent_body.get("actions") == {"markAsRead": True} # No conditions/exceptions/isEnabled should be in body when not provided. assert "conditions" not in sent_body assert "exceptions" not in sent_body assert "isEnabled" not in sent_body assert result.outputs_prefix == "MSGraphMail.Rule" assert result.outputs_key_field == "id" assert result.outputs["id"] == "AQAAAJ1=" assert result.outputs["displayName"] == "minimal rule" # @odata.* keys stripped for k in result.outputs: assert not k.startswith("@odata") def test_create_rule_happy_full(mocker): """ Given: - All supported fields supplied as JSON-string args. When: - Running create_rule_command with conditions, exceptions, is_enabled=true. Then: - The POST body contains all fields nested as Graph expects. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "user@example.com" actions = {"markAsRead": True, "moveToFolder": "AAAA="} conditions = {"subjectContains": ["invoice"]} exceptions = {"fromAddresses": [{"emailAddress": {"address": "boss@example.com"}}]} api_response = {"id": "1", "displayName": "full rule", "actions": actions} args = { "user_id": user_id, "display_name": "full rule", "sequence": 2, "is_enabled": "true", "actions": json.dumps(actions), "conditions": json.dumps(conditions), "exceptions": json.dumps(exceptions), } with requests_mock.Mocker() as m: mocked = m.post( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules", json=api_response, ) create_rule_command(client, args) sent_body = mocked.last_request.json() assert sent_body["displayName"] == "full rule" assert sent_body["sequence"] == 2 assert sent_body["isEnabled"] is True assert sent_body["actions"] == actions assert sent_body["conditions"] == conditions assert sent_body["exceptions"] == exceptions def test_create_rule_invalid_json_actions(): """ Given: - actions is an invalid JSON string. When: - Running create_rule_command. Then: - DemistoException is raised mentioning 'actions' and 'valid JSON'. """ client = self_deployed_client() args = {"user_id": "u", "display_name": "x", "sequence": 1, "actions": "{notValidJson"} with pytest.raises(DemistoException) as exc_info: create_rule_command(client, args) msg = str(exc_info.value) assert "actions" in msg assert "valid JSON" in msg def test_create_rule_empty_actions(): """ Given: - actions is an empty JSON object. When: - Running create_rule_command. Then: - DemistoException is raised mentioning 'non-empty'. """ client = self_deployed_client() args = {"user_id": "u", "display_name": "x", "sequence": 1, "actions": "{}"} with pytest.raises(DemistoException) as exc_info: create_rule_command(client, args) assert "non-empty" in str(exc_info.value) def test_update_rule_happy_partial(mocker): """ Given: - Only display_name is supplied. When: - Running update_rule_command. Then: - PATCH goes to /users/{user_id}/mailFolders/inbox/messageRules/{rule_id}. - Body contains exactly {"displayName": "..."} (no other keys). """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "user@example.com" rule_id = "RULE123" api_response = {"id": rule_id, "displayName": "renamed"} args = {"user_id": user_id, "rule_id": rule_id, "display_name": "renamed"} with requests_mock.Mocker() as m: mocked = m.patch( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules/{rule_id}", json=api_response, ) result = update_rule_command(client, args) assert mocked.call_count == 1 sent_body = mocked.last_request.json() assert sent_body == {"displayName": "renamed"} assert set(sent_body.keys()) == {"displayName"} assert result.outputs_prefix == "MSGraphMail.Rule" assert result.outputs_key_field == "id" def test_update_rule_happy_disable(mocker): """ Given: - is_enabled=false is supplied. When: - Running update_rule_command. Then: - PATCH body contains {"isEnabled": false}. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "user@example.com" rule_id = "RULE123" api_response = {"id": rule_id, "isEnabled": False} args = {"user_id": user_id, "rule_id": rule_id, "is_enabled": "false"} with requests_mock.Mocker() as m: mocked = m.patch( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules/{rule_id}", json=api_response, ) update_rule_command(client, args) sent_body = mocked.last_request.json() assert sent_body.get("isEnabled") is False def test_update_rule_happy_all_fields(mocker): """ Given: - All updatable fields are supplied. When: - Running update_rule_command. Then: - PATCH body contains all fields nested as Graph expects. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "user@example.com" rule_id = "RULE123" actions = {"markAsRead": True} conditions = {"subjectContains": ["foo"]} exceptions = {"sensitivity": "personal"} api_response = {"id": rule_id, "displayName": "all"} args = { "user_id": user_id, "rule_id": rule_id, "display_name": "all", "sequence": 5, "is_enabled": "true", "actions": json.dumps(actions), "conditions": json.dumps(conditions), "exceptions": json.dumps(exceptions), } with requests_mock.Mocker() as m: mocked = m.patch( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailFolders/inbox/messageRules/{rule_id}", json=api_response, ) update_rule_command(client, args) sent_body = mocked.last_request.json() assert sent_body["displayName"] == "all" assert sent_body["sequence"] == 5 assert sent_body["isEnabled"] is True assert "isReadOnly" not in sent_body assert sent_body["actions"] == actions assert sent_body["conditions"] == conditions assert sent_body["exceptions"] == exceptions def test_update_rule_empty_body_guard(): """ Given: - Only user_id and rule_id are supplied — no updatable fields. When: - Running update_rule_command. Then: - DemistoException is raised before any HTTP call is made. """ client = self_deployed_client() args = {"user_id": "u", "rule_id": "R"} with pytest.raises(DemistoException) as exc_info: update_rule_command(client, args) assert str(exc_info.value).startswith("At least one updatable field must be provided") def test_get_settings_happy(mocker): """ Given: - A realistic mailboxSettings payload from Graph. When: - Running get_mailbox_settings_command. Then: - GET goes to /users/{user_id}/mailboxSettings. - outputs['userId'] is synthesized to the requested user_id. - @odata.* keys are stripped from outputs. - CommandResults has the expected prefix and key field. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") user_id = "test@example.com" with open("test_data/mailbox_settings_response.json") as f: api_response = json.load(f) with requests_mock.Mocker() as m: mocked = m.get( f"https://graph.microsoft.com/v1.0/users/{user_id}/mailboxSettings", json=api_response, ) result = get_mailbox_settings_command(client, {"user_id": user_id}) assert mocked.call_count == 1 assert result.outputs_prefix == "MSGraphMail.MailboxSettings" assert result.outputs_key_field == "userId" assert result.outputs["userId"] == user_id # synthesized assert result.outputs["timeZone"] == "Pacific Standard Time" assert result.outputs["archiveFolder"] == api_response["archiveFolder"] # @odata.* keys stripped for k in result.outputs: assert not k.startswith("@odata") def test_get_mailtips_happy(mocker): """ Given: - A mailTips response with a single recipient. When: - Running get_mail_tips_command. Then: - POST goes to /users/{email_address}/getMailTips. - Body contains EmailAddresses=[email_address] and the full mailTipsOptions string. - outputs is a LIST of one item; @odata.* keys stripped. """ client = self_deployed_client() mocker.patch.object(client, "get_access_token") email_address = "test@example.com" with open("test_data/mail_tips_response.json") as f: api_response = json.load(f) with requests_mock.Mocker() as m: mocked = m.post( f"https://graph.microsoft.com/v1.0/users/{email_address}/getMailTips", json=api_response, ) result = get_mail_tips_command(client, {"email_address": email_address}) assert mocked.call_count == 1 sent_body = mocked.last_request.json() assert sent_body.get("EmailAddresses") == [email_address] expected_mail_tips_options = ( "automaticReplies,mailboxFullStatus,customMailTip,externalMemberCount," "totalMemberCount,maxMessageSize,deliveryRestriction,moderationStatus," "recipientScope,recipientSuggestions" ) assert sent_body.get("MailTipsOptions") == expected_mail_tips_options # Sanity-check a few discrete options are present in the option string. for opt in ("automaticReplies", "mailboxFullStatus", "recipientSuggestions"): assert opt in sent_body["MailTipsOptions"] assert result.outputs_prefix == "MSGraphMail.MailTips" assert result.outputs_key_field == "emailAddressValue" assert isinstance(result.outputs, list) assert len(result.outputs) == 1 assert result.outputs[0]["emailAddress"]["address"] == email_address assert result.outputs[0]["emailAddressValue"] == email_address for k in result.outputs[0]: assert not k.startswith("@odata") def test_get_mailtips_missing_email_address(): """ Given: - No email_address argument supplied. When: - Running get_mail_tips_command. Then: - DemistoException is raised mentioning 'email_address'. """ client = self_deployed_client() with pytest.raises(DemistoException) as exc_info: get_mail_tips_command(client, {}) assert "email_address" in str(exc_info.value) @pytest.mark.parametrize( "value, expected", [ (None, None), ("", None), ('{"k": 1}', {"k": 1}), ({"already": "dict"}, {"already": "dict"}), ([1, 2, 3], [1, 2, 3]), ], ) def test_parse_json_arg_valid(value, expected): """ Given: - Various valid inputs to parse_json_arg. When: - Parsing them. Then: - None/empty -> None; valid JSON string -> parsed object; already-parsed dict/list -> passthrough. """ assert parse_json_arg(value, "myarg") == expected def test_parse_json_arg_invalid_json_raises(): """ Given: - A malformed JSON string. When: - Calling parse_json_arg. Then: - DemistoException is raised mentioning the arg name and 'valid JSON'. """ with pytest.raises(DemistoException) as exc_info: parse_json_arg("{notValidJson", "myarg") msg = str(exc_info.value) assert "myarg" in msg assert "valid JSON" in msg def test_send_mail_with_attach_cids_uses_cid_labels_not_file_ids(mocker): """ Given: - send-mail command with attachIDs (War Room file IDs) and attachCIDs (CID labels) - The HTML body references inline images via CID (e.g., <img src="cid:mylogo"/>) When: - Sending a mail with inline image attachments Then: - The attachCIDs values should be used as contentId labels (not as file IDs for getFilePath) - The files referenced by attachIDs should be marked as inline when they have a corresponding CID - No call to getFilePath should be made with CID labels - The email should be sent successfully with inline images """ client = self_deployed_client() args = { "to": ["recipient@example.com"], "htmlBody": '<html><body>Hello <img src="cid:mylogo"/> World</body></html>', "subject": "test with inline CID", "from": "sender@example.com", "attachIDs": "15@8", "attachCIDs": "mylogo", } # Mock getFilePath to return a valid file for the War Room file ID "15@8" mocker.patch.object( demisto, "getFilePath", return_value={"path": "test_data/plant.jpg", "name": "plant.jpg"}, ) with requests_mock.Mocker() as request_mocker: mocker.patch.object(client, "get_access_token") send_mail_mocker = request_mocker.post(f"https://graph.microsoft.com/v1.0/users/{args['from']}/SendMail") send_email_command(client, args) assert send_mail_mocker.called message = send_mail_mocker.last_request.json().get("message") assert message message_attachments = message.get("attachments", []) # Verify the attachment is marked as inline with the correct CID assert len(message_attachments) == 1 attachment = message_attachments[0] assert attachment["isInline"] is True assert attachment["contentId"] == "mylogo" assert attachment["name"] == "plant.jpg" # Verify getFilePath was called with the War Room file ID, NOT the CID label demisto.getFilePath.assert_called_once_with("15@8") def test_send_mail_with_attach_ids_no_cids_are_not_inline(mocker): """ Given: - send-mail command with attachIDs but no attachCIDs When: - Sending a mail with regular (non-inline) attachments Then: - The attachments should NOT be marked as inline - The contentId should be the file ID (backward compatibility) """ client = self_deployed_client() args = { "to": ["recipient@example.com"], "htmlBody": "<html><body>Hello World</body></html>", "subject": "test without CIDs", "from": "sender@example.com", "attachIDs": "15@8", } mocker.patch.object( demisto, "getFilePath", return_value={"path": "test_data/plant.jpg", "name": "plant.jpg"}, ) with requests_mock.Mocker() as request_mocker: mocker.patch.object(client, "get_access_token") send_mail_mocker = request_mocker.post(f"https://graph.microsoft.com/v1.0/users/{args['from']}/SendMail") send_email_command(client, args) assert send_mail_mocker.called message = send_mail_mocker.last_request.json().get("message") assert message message_attachments = message.get("attachments", []) assert len(message_attachments) == 1 attachment = message_attachments[0] assert attachment["isInline"] is False assert attachment["contentId"] == "15@8"