Gmail Single User

Gmail API using OAuth 2.0.

Email · Gmail Single User

Details

IDGmail Single User
ProviderGoogle
CategoryEmail
From Version5.0.0
Docker Imagedemisto/google-api-py3:1.0.0.10182333
Supported ModulesAgentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud

README

Use the Gmail Single User integration to send emails and fetch emails as incidents to Cortex XSOAR.

Note: Use this integration if you only want to fetch and send emails from a single user’s mailbox. If you need to access multiple user mailboxes, use the GMail Integration.

Configure Gmail Single User on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Gmail Single User.
  3. Click Add instance to create and configure a new integration instance.
Parameter Name Description Required
email Gmail of the user True
send_as Allows you to specify the e-mail address from which to send e-mails from False
code Auth Code (run the !gmail-auth-link command to start the auth flow - see the Application Authorization Flow section) False
client_id Client ID (see Application Authorization Flow section) True
client_secret Client Secret (see Application Authorization Flow section) True
redirect_uri Auth Redirect URI (see Application Authorization Flow section) True
isFetch Fetch incidents False
fetch_time First fetch timestamp, in days False
query Events query (e.g., “from:example@demisto.com”) False
fetch_limit Maximum number of emails to pull per fetch. Default is 50.
The maximum number is 200 emails per fetch (even if a higher number is configured).
False
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False

Application Authorization Flow

  • To allow Cortex XSOAR to access Gmail, you will need create your own OAuth2 app.
  • Once you have the app, follow the steps in Authorization Flow In Cortex XSOAR to configure the OAuth 2.0 authorization.

Create Your OAuth2 App

To use the GMail Single User integration, you will need to create your own OAuth2 Google app.

  1. Go to the developers credentials page (you may need to set up a new project if you haven’t already).
  2. If needed, configure the Consent Screen. Fill in the Consent Screen information you want to display to your users.
  3. Make sure in the consent screen that you publish the app by clicking Publish App and confirming.
    OAuth-Consent-Screen-Publication
  4. In the credentials page choose: Create Credentials -> OAuth client ID.
    Create Credentials
  5. When creating the OAuth client ID, select Web application as the application type.
  6. Name the credential. For example: Cortex XSOAR GMail Integration.
  7. Add an Authorized redirect URIs: https://oproxy.demisto.ninja/authcode. The oproxy url is a client side only web page which provides an easy interface to copy the obtained auth code from the authorization respone to the integration configuration in the authorization flow steps. Optionally: if you don’t want to use the oproxy url, you may use a localhost url on a port which is not used locally on your machine. For example: http://localhost:9004. You will then need to copy the code from the url address bar in the response (see Authorization Flow In Cortex XSOAR).
    Authorized URIs
  8. Make sure to enable the Gmail API if you haven’t already.
  9. After you create the app, copy the client id and client secret of the app that you created to the integration configuration. If you want to use a localhost redirect URI, copy the chosen auth redirect URI to the integration configuration.
  10. Proceed to Authorization Flow In Cortex XSOAR to configure OAuth 2.0 authorization in Cortex XSOAR.

Deprecated: Use the Demisto App for GSuite Admins (OOB Authorization)

Note: This is an old method of authentication which was provided for GSuite Admins via the use of the Demisto App. This method uses the OOB authentication flow which has been deprecated by Google. This method of authentication will stop working on Jan 31, 2023. Instructions are provided here for backwards compatibility until the official end of life of OOB authentication. All customers must migrate to the web app authorization flow using their own Google App as documented at Create Your OAuth2 App. For further details See:

  • https://developers.google.com/identity/protocols/oauth2/resources/oob-migration
  • https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html

To configure the use of the Demisto App for GSuite Admins there is no need to create your own app (you will be using the predefined Demisto App). Thus, there is no need to fill in the Client ID and Client Secret configuration fields.

GSuite Admins can choose to trust the Demisto App so users can configure the app:

  1. Go to App Access Control.
  2. Choose: Configure new app -> OAuth App Name Or Client ID.
    GSuite App Configurations
  3. Enter the following Client ID: 391797357217-pa6jda1554dbmlt3hbji2bivphl0j616.apps.googleusercontent.com
    You see the Demisto App in the results page.
    Demisto App
  4. Select the app and grant the app access as Trusted.
  5. Add the Demisto app client ID 391797357217-pa6jda1554dbmlt3hbji2bivphl0j616.apps.googleusercontent.com to the integration configuration.
  6. Proceed to ‘Authorization Flow In Cortex XSOAR’ to configure OAuth 2.0 authorization in Cortex XSOAR.

Authorization Flow In Cortex XSOAR

  1. Create and save an integration instance of the Gmail Single User integration. Do not fill in the Auth Code field, this is obtained in the next steps.
  2. To obtain the Auth Code, run the following command in the playground: !gmail-auth-link. Access the link you receive to authenticate your Gmail account.
  3. If you get a message from Google saying that it cannot verify the application, click proceed and click enter for the app name to give the app you created permissions to your Google account. Then click proceed.
  4. Complete the authentication process.
  5. If using the oproxy redirect URI, you will be redirected to a page which will provide an option to copy the auth code. Note that this page processes the code fully in the browser (client side) and displays it to be copied. The code is not transmitted to the oproxy web server. Example screenshot:
    Oproxy Auth Code
    If you are using the localhost redirect URI, you will be redirected to an unavailable page. Copy the code, including the code= prefix up to the first & from the URL address bar. Example screenshot:
    localhost Auth Code
  6. Copy the received code to the Auth Code configuration parameter of the integration instance.
  7. Save the instance.
  8. To verify that authentication was configured correctly, run the !gmail-auth-test.

Fetched Incidents Data

  • Incident Name
  • Occurred
  • Owner
  • Type
  • Severity
  • Email From
  • Email Message ID
  • Email Subject
  • Email To
  • Attachment Extension
  • Attachment Name
  • Email Body
  • Email Body Format

Commands

You can execute these commands from the Cortex XSOAR 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.

send-mail

Sends an email using Gmail.

Base Command

send-mail

Input
Argument Name Description Required
to The email addresses of the receiver. Required
body The content (body) of the email to be sent in plain text. Optional
subject The subject of the email to be sent. Required
attachIDs A comma-separated list of IDs of War Room entries that contain files, which need be attached to the email. Optional
cc The CC additional recipient email address. Optional
bcc The BCC additional recipient email address. Optional
htmlBody The content (body) of the email to be sent in HTML format. Optional
replyTo The email address used to reply to the message. Optional
attachNames A comma-separated list of new names for attachments, according to the order they were attached to the email.
For example, to rename the first and third file: attachNames=new_fileName1,,new_fileName3
To rename the second and fifth files: attachNames=,new_fileName2,,,new_fileNam
Optional
attachCIDs A comma-separated list of CID images to embed attachments in the email. Optional
transientFile The text name for an attached file. Multiple files are supported as a comma-separated list. For example, transientFile=”t1.txt,temp.txt,t3.txt” transientFileContent=”test 2,temporary file content,third file content” transientFileCID=”t1.txt@xxx.yyy,t2.txt@xxx.zzz”. Optional
transientFileContent The content for the attached file. Multiple files are supported as a comma-separated list. For example, transientFile=”t1.txt,temp.txt,t3.txt” transientFileContent=”test 2,temporary file content,third file content” transientFileCID=”t1.txt@xxx.yyy,t2.txt@xxx.zzz”. Optional
transientFileCID The CID image for an attached file to include within the email body. Multiple files are supported as comma-separated list. For example, transientFile=”t1.txt,temp.txt,t3.txt” transientFileContent=”test 2,temporary file content,third file content” transientFileCID=”t1.txt@xxx.yyy,t2.txt@xxx.zzz”. Optional
additionalHeader A comma-separated list of additional headers in the format headerName=headerValue. For example, “headerName1=headerValue1,headerName2=headerValue2”. Optional
templateParams ‘Replaces {varname} variables with values from this parameter. Expected values are in the form of a JSON document. For example, {“varname” :{“value” “some value”, “key”: “context key”}}. Each var name can either be provided with the value or a context key to retrieve the value. Note that only context data is accessible for this argument, while incident fields are not.’ Optional
Context Output
Path Type Description
Gmail.SentMail.ID String The immutable ID of the message.
Gmail.SentMail.Labels String A list of label IDs applied to this message.
Gmail.SentMail.ThreadId String The ID of the thread in which the message belongs.
Gmail.SentMail.To String The email recipient.
Gmail.SentMail.From String The email sender.
Gmail.SentMail.Cc String The additional CC recipient email address.
Gmail.SentMail.Bcc String The additional BCC recipient email address.
Gmail.SentMail.Subject String The email subject.
Gmail.SentMail.Body String The plain-text version of the email.
Gmail.SentMail.MailBox String The mailbox from which the mail was sent.
Gmail.SentMail.BodyHTML String The HTML version of the email.
Command Example

!send-mail subject="this is the subject" to=test@demistodev.com body="this is the body"

Context Example
{
    "Gmail.SentMail": [
 {
     "Body": "this is the body", 
     "From": "example@demisto.com", 
     "Cc": null, 
     "Labels": [
  "SENT"
     ], 
     "Bcc": null, 
     "To": "test@demistodev.com", 
     "ThreadId": "16f662789d3a2972", 
     "Mailbox": "test@demistodev.com", 
     "Type": "Gmail", 
     "ID": "16f662789d3a2972", 
     "Subject": "this is the subject"
 }
    ]
}
Human Readable Output

Email sent

Type ID To From Subject Body Labels ThreadId
Gmail 16f662789d3a2972 test@demistodev.com example@demisto.com this is the subject this is the body SENT 16f662789d3a2972

gmail-auth-link


Starts the OAuth2 process. Gets a link to use to authenticate to Gmail.

Base Command

gmail-auth-link

Input

There is no input for this command.

Context Output

There is no context output for this command.

Command Example


#### Human Readable Output

## Gmail Auth Link

Please follow the following **link**.
After Completing the authentication process, copy the received code
to the **Auth Code** configuration parameter of the integration instance.
Save the integration instance and then run *!gmail-auth-test* to test that
the authentication is properly set.

### gmail-auth-test

***
Tests that Gmail auth is configured properly. Use this command after completing the OAuth2 authentication process.

#### Base Command

`gmail-auth-test`

#### Input

There is no input for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!gmail-auth-test```

#### Human Readable Output

Authentication test completed successfully.

### reply-mail

***
Replies to a mail using Gmail.

#### Limitations

The *subject* argument should be the same as the subject of the email you are replying to in order for the reply to be a part of the same conversation.

#### Base Command

`reply-mail`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| to | Email addresses of the recipients. | Required |
| from | Email address of the sender. | Optional |
| body | The contents (body) of the email to be sent in plain text. | Optional |
| subject | Subject of the email to be sent. <br/> Should be the same as the subject of the email you are replying to in order for the reply to be a part of the same conversation. | Required |
| inReplyTo | A comma-separated list of message IDs to reply to. | Required |
| references | A comma-separated list of message IDs to refer to. | Optional |
| attachIDs | A comma-separated list of IDs of War Room entries that contain the files that need be attached to the email. | Optional |
| cc | Additional recipient email addresses (CC). | Optional |
| bcc | Additional recipient email addresses (BCC). | Optional |
| htmlBody | The contents (body) of the email to be sent in HTML format. | Optional |
| replyTo | Address that needs to be used to reply to the message. | Optional |
| attachNames | A comma-separated list of new names to rename attachments corresponding to the order in which they were attached to the email.<br/>        Examples - To rename first and third file attachNames=new_fileName1,,new_fileName3<br/>        To rename second and fifth files attachNames=,new_fileName2,,,new_fileName5 | Optional |
| attachCIDs | A comma-separated list of CID images to embed attachments inside the email. | Optional |
| transientFile | Textual name for an attached file. Multiple files are supported as a<br/>        comma-separated list. For example, transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test<br/>        2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz" | Optional |
| transientFileContent | Content for the attached file. Multiple files are supported as a comma-separated<br/>        list. For example, transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test<br/>        2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz" | Optional |
| transientFileCID | CID image for an attached file to include within the email body. Multiple files are<br/>        supported as comma-separated list. For example, transientFile="t1.txt,temp.txt,t3.txt"<br/>        transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz" | Optional |
| additionalHeader | A comma-separated list of additional headers in the format: headerName=headerValue. For example: "headerName1=headerValue1,headerName2=headerValue2". | Optional |
| templateParams | 'Replaces {varname} variables with values from this parameter. Expected<br/>       values are in the form of a JSON document. For example, {"varname" :{"value" "some<br/>       value", "key": "context key"}}. Each var name can either be provided with<br/>       the value or a context key to retrieve the value.<br/> Note that only context data is accessible for this argument, while incident fields are not.' | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Gmail.SentMail.ID | String | The immutable ID of the message. |
| Gmail.SentMail.Labels | String | List of IDs of the labels applied to this message. |
| Gmail.SentMail.ThreadId | String | The ID of the thread in which the message belongs. |
| Gmail.SentMail.To | String | The recipients of the email. |
| Gmail.SentMail.From | Unknown | The sender of the email. |
| Gmail.SentMail.Cc | String | Additional recipient email addresses \(CC\). |
| Gmail.SentMail.Bcc | String | Additional recipient email addresses \(BCC\). |
| Gmail.SentMail.Subject | String | The subject of the email. |
| Gmail.SentMail.Body | Unknown | The plain-text version of the email. |
| Gmail.SentMail.MailBox | String | The mailbox from which the mail was sent. |

#### Command Example

```!reply-mail subject="this is the subject" to=test@demistodev.com replyTo=test@demistodev.com body="this is the body" inReplyTo=<CAEvnzx+zEeFJ1U5g4FOfHKeWe-H3hU7kGiKaK7q0F0A@mail.gmail.com> references=<CAEvnzx+zEeFJ1U5g4FOfHKeWe-H3hU7kGiKaK7q0F0A@mail.gmail.com>```

#### Context Example

{
“Gmail.SentMail”: [
{
“Bcc”: null,
“Body”: “this is the body”,
“Cc”: null,
“From”: “admin@demistodev.com”,
“ID”: “16d43287fc29b71a”,
“Labels”: [
“SENT”
],
“Mailbox”: “test@demistodev.com”,
“Subject”: “this is the subject”,
“ThreadId”: “16d43287fc29b71a”,
“To”: “test@demistodev.com”,
“Type”: “Gmail”
}
]
}
```

Human Readable Output

Email sent

Type ID To From Subject Body Labels ThreadId
Gmail 16d43287fc29b71a test@demistodev.com admin@demistodev.com this is the subject this is the body SENT 16d43287fc29b71a

gmail-get-attachments


Retrieves attachments from a sent Gmail message.

Base Command

gmail-get-attachments

Input

Argument Name Description Required
message-id The ID of the message to retrieve. Required
user-id The user’s email address. The “me” special value can be used to indicate the authenticated user. Required

Command Example

!gmail-get-attachments message-id=16d4316a25a332e4 user-id=admin@demistodev.com

gmail-get-incidents


Retrieves a list of emails from Gmail within a specific date range and formats them as incidents.

Base Command

gmail-get-incidents

Input

Argument Name Description Required
max_results Maximum number of emails to retrieve. Default is 10. Optional
labels List of label IDs to filter emails. Optional
query Additional Gmail search query string. Optional
subject Filter emails by subject. Optional
from Filter emails sent from a specific sender. Optional
to Filter emails sent to a specific recipient. Optional
filename Filter emails by attached filename. Optional
in Specify the email folder to search (e.g., ‘inbox’, ‘sent’). Optional
has_attachments Whether to filter emails that contain attachments. Possible values are: true, false. Default is false. Optional
before Filter emails before this date. Optional
after Filter emails after this date. Optional

Context Output

There is no context output for this command.

Command Example

!gmail-get-incidents from="admin@dummy.com"

Configuration parameters

  • email — Email of user (required)
  • send_as — Send as
  • credentials — Client ID
  • client_id — Client ID
  • client_secret — Client Secret
  • redirect_uri — Auth Redirect URI
  • auth_code_creds
  • code — Auth Code (run the !gmail-auth-link command to start the auth flow - see Help section)
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents
  • fetch_time — First fetch timestamp, in days.
  • query — Events query (e.g., "from:example@demisto.com")
  • fetch_limit — Maximum number of emails to pull per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • legacy_name — Use legacy attachment name

Commands (6)

  • gmail-auth-link

    Starts the OAuth2 process. Get a link to use to authenticate to Gmail.

  • gmail-auth-test

    Tests that Gmail auth is configured properly. Use this command after completing the OAuth2 authentication process.

  • gmail-get-attachments

    Retrieves attachments from a sent Gmail message.

  • gmail-get-incidents

    Retrieves a list of emails from Gmail within a specific date range and formats them as incidents.

  • reply-mail

    Replies to a mail using Gmail.

  • send-mail

    Sends an email using Gmail.

import json
import uuid
import pytest
from pytest_mock import MockerFixture
import demistomock as demisto
from datetime import datetime, UTC
from freezegun import freeze_time


from GmailSingleUser import Client, send_mail_command, MIMEMultipart, execute_gmail_action
from email.utils import parsedate_to_datetime


@pytest.fixture
def gmail_client(mocker):
    client = Client()
    mocker.patch.object(client, "get_access_token", return_value="token")
    return client


class MockAttachments:
    def get(self, **kwargs):
        return MockExecute()


class MockExecute:
    def execute(self):
        return {"attachmentId": "67890", "size": 1024, "data": "mock_data"}


class MockSend:
    def execute(self):
        return {"id": "12345"}


class MockGet:
    def execute(self):
        return {"id": "12345", "snippet": "Test email content"}


class MockList:
    def execute(self):
        return {"messages": [{"id": "12345"}, {"id": "67890"}]}


class MockMessages:
    def send(self, **kwargs):
        return MockSend()

    def get(self, **kwargs):
        return MockGet()

    def attachments(self, **kwargs):
        return MockAttachments()

    def list(self, **kwargs):
        return MockList()


class MockUsers:
    def messages(self):
        return MockMessages()


class MockService:
    def users(self):
        return MockUsers()


@pytest.fixture
def mock_service():
    return MockService()


def test_execute_gmail_action_send(mock_service):
    result = execute_gmail_action(mock_service, "send", {})
    assert result == {"id": "12345"}


def test_execute_gmail_action_get(mock_service):
    result = execute_gmail_action(mock_service, "get", {})
    assert result == {"id": "12345", "snippet": "Test email content"}


def test_execute_gmail_action_get_attachments(mock_service):
    result = execute_gmail_action(mock_service, "get_attachments", {})
    assert result == {"attachmentId": "67890", "size": 1024, "data": "mock_data"}


def test_execute_gmail_action_list(mock_service):
    result = execute_gmail_action(mock_service, "list", {})
    assert result == {"messages": [{"id": "12345"}, {"id": "67890"}]}


def test_execute_gmail_action_unsupported(mock_service):
    action_kwargs = {}
    with pytest.raises(ValueError, match="Unsupported action: unsupported_action"):
        execute_gmail_action(mock_service, "unsupported_action", action_kwargs)


MOCK_MAIL_NO_LABELS = {
    "internalDate": "1572251535000",
    "historyId": "249781",
    "payload": {
        "mimeType": "multipart/mixed",
        "body": {"size": 0},
        "partId": "",
        "filename": "",
        "headers": [
            {
                "name": "Received",
                "value": "from 1041831412594 named unknown by gmailapi.google.com with "
                "HTTPREST; Mon, 28 Oct 2019 04:32:15 -0400",
            },
            {"name": "Content-Type", "value": 'mixed; boundary="===============4922146810840031257=="'},
            {"name": "MIME-Version", "value": "1.0"},
            {"name": "to", "value": "<some_mail>"},
            {"name": "cc", "value": ""},
            {"name": "bcc", "value": ""},
            {"name": "from", "value": "<some_mail>"},
            {"name": "subject", "value": "a mail subject"},
            {"name": "reply-to", "value": ""},
            {"name": "Date", "value": "Mon, 28 Oct 2019 04:32:15 -0400"},
            {"name": "Message-Id", "value": "<some_id>"},
        ],
        "parts": [
            {
                "mimeType": "text/plain",
                "headers": [
                    {"name": "Content-Type", "value": 'text/plain; charset="utf-8"'},
                    {"name": "MIME-Version", "value": "1.0"},
                    {"name": "Content-Transfer-Encoding", "value": "base64"},
                ],
                "body": {"data": "<data>", "size": 9},
                "partId": "0",
                "filename": "",
            }
        ],
    },
    "snippet": "some info",
    "sizeEstimate": 637,
    "threadId": "<id>",
    "id": "<id>",
}


EXPECTED_GMAIL_CONTEXT = {
    "To": "<some_mail>",
    "Body": "",
    "From": "<some_mail>",
    "Attachments": "",
    "Format": "mixed",
    "Cc": "",
    "Labels": "",
    "Mailbox": "some_mail",
    "Headers": [
        {
            "Name": "Received",
            "Value": "from 1041831412594 named unknown by gmailapi.google.com with HTTPREST; Mon, 28 Oct 2019 04:32:15 -0400",
        },
        {"Name": "Content-Type", "Value": 'mixed; boundary="===============4922146810840031257=="'},
        {"Name": "MIME-Version", "Value": "1.0"},
        {"Name": "to", "Value": "<some_mail>"},
        {"Name": "cc", "Value": ""},
        {"Name": "bcc", "Value": ""},
        {"Name": "from", "Value": "<some_mail>"},
        {"Name": "subject", "Value": "a mail subject"},
        {"Name": "reply-to", "Value": ""},
        {"Name": "Date", "Value": "Mon, 28 Oct 2019 04:32:15 -0400"},
        {"Name": "Message-Id", "Value": "<some_id>"},
    ],
    "Html": None,
    "RawData": None,
    "ThreadId": "<id>",
    "Date": "Mon, 28 Oct 2019 04:32:15 -0400",
    "Bcc": "",
    "Type": "Gmail",
    "ID": "<id>",
    "Subject": "a mail subject",
}


def test_header_to_date():
    valid_header_date = Client.get_date_from_email_header("Mon, 26 Aug 2019 14:40:04 +0300")
    semi_valid_header_date = Client.get_date_from_email_header("26 Aug 2019 14:40:04 +0300")
    header_date_no_tz = Client.get_date_from_email_header("26 Aug 2019 11:40:04")
    header_x_received = Client.get_date_from_email_header(
        "by 2002:a17:90a:77cb:0:0:0:0 with SMTP id e11csp4670216pjs;        Mon, 26 Aug 2019 03:40:04 -0800 (PST)"
    )
    # all should be the same
    assert valid_header_date == semi_valid_header_date
    assert valid_header_date == header_date_no_tz
    assert header_x_received == valid_header_date


def test_no_label_mail_context_creation():
    client = Client()
    context_gmail, _, _, occurred, is_internal = client.get_email_context(MOCK_MAIL_NO_LABELS, "some_mail")
    assert int(occurred.timestamp()) == 1572251535
    assert is_internal
    assert context_gmail.get("Labels") == EXPECTED_GMAIL_CONTEXT.get("Labels")
    assert context_gmail.get("To") == EXPECTED_GMAIL_CONTEXT.get("To")
    assert context_gmail.get("From") == EXPECTED_GMAIL_CONTEXT.get("From")
    assert context_gmail.get("Subject") == EXPECTED_GMAIL_CONTEXT.get("Subject")


def test_extract_occurred_no_headers():
    occurred, is_valid = Client.get_occurred_date(
        {"payload": {"headers": [{"name": "stam", "value": "stam"}]}, "internalDate": "1610642469000"}
    )
    assert is_valid
    assert occurred.timestamp() == 1610642469


def test_no_date_mail():
    with open("test_data/email_no_date.json") as f:
        msg = json.load(f)
    client = Client()
    context_gmail, _, _, occurred, is_valid = client.get_email_context(msg, "some_mail")
    # check that the x-received date was usd
    assert occurred.timestamp() == parsedate_to_datetime("Mon, 21 Dec 2020 12:11:57 -0800").timestamp()
    assert is_valid
    assert context_gmail.get("Date") == "Mon, 21 Dec 2020 12:11:57 -0800"


def test_generate_auth_link_oob():
    client = Client()
    link = client.generate_auth_link()
    assert link.startswith("https://accounts.google.com/o/oauth2/v2/auth?")
    assert "code_challenge=" in link
    assert "code_challenge_method=S256" in link


def test_generate_auth_link_web(mocker):
    mocker.patch("GmailSingleUser.CLIENT_SECRET", "test")
    mocker.patch("GmailSingleUser.CLIENT_ID", "test_id")
    mocker.patch("GmailSingleUser.REDIRECT_URI", "http://localhost:9001")
    client = Client()
    link = client.generate_auth_link()
    assert link.startswith("https://accounts.google.com/o/oauth2/v2/auth?")
    assert "code_challenge=" not in link
    assert "code_challenge_method=S256" not in link
    assert "access_type=offline" in link
    assert "redirect_uri=http" in link
    assert "client_id=test_id" in link


SEND_EMAIL_ARGS = [
    {"subject": "test", "to": "test@gmail.com", "body": "hello"},
    {"subject": "test", "to": "test@gmail.com", "htmlBody": "<h1>test</h1>"},
]


@pytest.mark.parametrize("command_args", SEND_EMAIL_ARGS)
def test_send_mail(gmail_client, mocker, command_args):
    """
    Given:
        - send mail command arguments

    When:
        - executing the send mail function

    Then:
        - a valid entry is returned
    """
    mocker.patch.object(gmail_client, "send_email_request", return_value={"id": "123", "threadId": "123", "labelIds": ["SENT"]})
    mocker.patch.object(demisto, "args", return_value=command_args)

    send_email_entry = send_mail_command(client=gmail_client)
    context_output = send_email_entry["EntryContext"]
    assert "Gmail.SentMail(val.ID && val.Type && val.ID == obj.ID && val.Type == obj.Type)" in context_output
    context_output = context_output["Gmail.SentMail(val.ID && val.Type && val.ID == obj.ID && val.Type == obj.Type)"][0]

    assert context_output.get("To") == command_args.get("to")
    assert context_output.get("Subject") == command_args.get("subject")
    assert context_output.get("Body") == command_args.get("body")
    assert context_output.get("BodyHTML") == command_args.get("htmlBody")
    assert context_output.get("Mailbox") == command_args.get("to")

    markdown_table = send_email_entry["HumanReadable"]
    assert "Gmail" in markdown_table
    assert command_args.get("to") in markdown_table
    assert command_args.get("subject") in markdown_table
    assert "SENT" in markdown_table


def test_send_mail_with_reference(gmail_client: Client, mocker):
    """
    Given:
        - The references argument as a list type
        - The inReplyTo argument as a str type
    When:
        - run client.send_mail function
    Then:
        - Ensure the function expects to receive the type
          of the reference argument and the inReplyTo argument
    """
    mocker.patch.object(gmail_client, "send_email_request", return_value=True)
    assert gmail_client.send_mail(
        emailto=None,
        emailfrom=None,
        send_as=None,
        cc=None,
        bcc=None,
        subject=None,
        body=None,
        htmlBody=None,
        entry_ids=None,
        replyTo=None,
        file_names=None,
        attach_cid=None,
        manualAttachObj=None,
        transientFile=None,
        transientFileContent=None,
        transientFileCID=None,
        additional_headers=None,
        templateParams=None,
        references=["test", "test1"],
        inReplyTo="test test",
    )


def test_send_mail_MIMEMultipart_constructor(mocker: MockerFixture):
    """
    Given:
        - Client object
    When:
        - Client constructor called
    Then:
        - Ensure MIMEMultipart constructor called once without subtype
    """
    from GmailSingleUser import Client
    import GmailSingleUser

    gmail_single_user_client = Client()
    # Mock the chain of calls: service.users().messages().send().execute()
    mock_execute = mocker.Mock(return_value={"id": "mock_message_id"})
    mock_send = mocker.Mock(return_value=mock_execute)
    mock_messages = mocker.Mock(send=mocker.Mock(return_value=mock_send))
    mock_users = mocker.Mock(messages=mocker.Mock(return_value=mock_messages))
    mock_service = mocker.Mock(users=mocker.Mock(return_value=mock_users))
    # Patch the service object in the Client class to use the mocked service
    mocker.patch.object(GmailSingleUser.Client, "get_service", new=mock_service)
    # Replace MIMEMultipart with the mock object
    mocker_obj = mocker.patch.object(GmailSingleUser, "MIMEMultipart", return_value=MIMEMultipart())

    gmail_single_user_client.send_mail(
        emailto="test@gmail.com",
        emailfrom="test@gmail.com",
        send_as="test@gmail.com",
        cc=None,
        bcc=None,
        subject="hello-world",
        body="body",
        htmlBody="<>",
        entry_ids=[],
        replyTo=None,
        file_names=[],
        attach_cid=[],
        manualAttachObj=[],
        transientFile=[],
        transientFileContent=[],
        transientFileCID=[],
        additional_headers=[],
        templateParams=None,
    )

    mocker_obj.assert_called_once()
    assert mocker_obj.call_args.args == ()


def test_handle_html(mocker):
    """
    Given:
        - html body of a message.
    When:
        - run handle_html function.
    Then:
        - Ensure attachments list contains 2 items with correct data, name and cid fields.
    """
    client = Client()
    mocker.patch.object(demisto, "uniqueFile", return_value="1234567")
    mocker.patch.object(demisto, "getFilePath", return_value={"path": "", "name": ""})
    mocker.patch.object(uuid, "uuid4", return_value="11111111")
    htmlBody = """<html>
                        <body>
                            <img src="data:image/png;base64,Aa=="/>
                            <img src="data:image/jpeg;base64,Bb=="/>
                        </body>
                      </html>"""

    expected_attachments = [
        {
            "maintype": "image",
            "subtype": "png",
            "data": b"\x01",
            "name": "image0.png",
            "cid": "image0.png@11111111_11111111",
            "ID": "image0.png@11111111_11111111",
        },
        {
            "maintype": "image",
            "subtype": "jpeg",
            "data": b"\x05",
            "name": "image1.jpeg",
            "cid": "image1.jpeg@11111111_11111111",
            "ID": "image1.jpeg@11111111_11111111",
        },
    ]
    expected_cleanBody = '<html>\n                        <body>\n                            <img src="cid:image0.png@11111111_11111111"/>\n                            <img src="cid:image1.jpeg@11111111_11111111"/>\n                        </body>\n                      </html>'  # noqa: E501
    cleanBody, attachments = client.handle_html(htmlBody)

    assert expected_cleanBody == cleanBody
    assert expected_attachments == attachments


def test_handle_html_image_with_new_line(mocker):
    """
    Given:
        - html body of a message with an attached base64 image.
    When:
        - run handle_html function.
    Then:
        - Ensure attachments list contains correct data, name and cid fields.
    """
    client = Client()
    mocker.patch.object(demisto, "uniqueFile", return_value="1234567")
    mocker.patch.object(demisto, "getFilePath", return_value={"path": "", "name": ""})
    mocker.patch.object(uuid, "uuid4", return_value="11111111")
    htmlBody = """
<html>
    <body>
        <img\n\t\t\t\t\t  src="data:image/png;base64,Aa=="/>
    </body>
</html>"""

    expected_attachments = [
        {
            "maintype": "image",
            "subtype": "png",
            "data": b"\x01",
            "name": "image0.png",
            "cid": "image0.png@11111111_11111111",
            "ID": "image0.png@11111111_11111111",
        }
    ]
    expected_cleanBody = (
        '\n<html>\n    <body>\n        <img\n\t\t\t\t\t  src="cid:image0.png@11111111_11111111"/>\n    </body>\n</html>'  # noqa: E501
    )

    cleanBody, attachments = client.handle_html(htmlBody)

    assert expected_cleanBody == cleanBody
    assert expected_attachments == attachments


part_test1 = [
    {
        "filename": "image-1.png",
        "headers": [{"name": "Content-ID", "value": "<5678>"}, {"name": "Content-Disposition", "value": "inline"}],
        "body": {"attachmentId": "1234"},
        "mimeType": "",
    }
]

part_test2 = [
    {
        "filename": "image-1.png",
        "headers": [{"name": "Content-ID", "value": "5678"}, {"name": "Content-Disposition", "value": "attachment"}],
        "body": {"attachmentId": "1234"},
        "mimeType": "",
    }
]

part_test3 = [
    {
        "filename": "image-1.png",
        "headers": [{"name": "Content-ID", "value": "None"}, {"name": "Content-Disposition", "value": "attachment"}],
        "body": {"attachmentId": "1234"},
        "mimeType": "",
    }
]


@pytest.mark.parametrize(
    "part, expected_result",
    [
        (part_test1, ("", "", [{"ID": "1234", "Name": "5678-attachmentName-image-1.png"}])),
        (part_test2, ("", "", [{"ID": "1234", "Name": "image-1.png"}])),
        (part_test3, ("", "", [{"ID": "1234", "Name": "image-1.png"}])),
    ],
)
def test_parse_mail_parts(part, expected_result):
    """
    Given:
        - Part of message from Gmail API response.
    When:
        - Run parse_mail_parts function with LEGACY_NAME is false.
    Then:
        - Ensure attachment's name was correctly constructed and parsing was correctly done.
    """
    client = Client()
    result = client.parse_mail_parts(part)
    assert result == expected_result


@pytest.mark.parametrize(
    "part, expected_result",
    [
        (part_test1, ("", "", [{"ID": "1234", "Name": "image-1.png"}])),
        (part_test2, ("", "", [{"ID": "1234", "Name": "image-1.png"}])),
        (part_test3, ("", "", [{"ID": "1234", "Name": "image-1.png"}])),
    ],
)
def test_parse_mail_parts_use_legacy_name(monkeypatch, part, expected_result):
    """
    Given:
        - Part of message from Gmail API response.
    When:
        - Run parse_mail_parts function LEGACY_NAME is true.
    Then:
        - Ensure attachment's name was correctly constructed and parsing was correctly done.
    """
    client = Client()
    monkeypatch.setattr("GmailSingleUser.LEGACY_NAME", True)
    result = client.parse_mail_parts(part)
    assert result == expected_result


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def test_get_incidents_command(mocker):
    from GmailSingleUser import Client, get_incidents_command

    client = Client()
    args = {"after": "24 Mar 2025 08:17:02 -0700", "before": "24 Mar 2025 08:18:02 -0700"}
    mocker.patch.object(demisto, "args", return_value=args)
    mocker.patch.object(
        client,
        "search",
        return_value=(
            util_load_json("test_data/search_email_list.json"),
            "before:1742829482  after:1742829422",
        ),
    )
    result = get_incidents_command(client)
    emails: list = result.raw_response  # type: ignore
    assert emails[0].get("internalDate") == "1742829478000"
    assert len(emails) == 2
    assert "2025-03-24T15:17:58.000Z | SENT | 111 |\n| test@test.com | Test message 1234 | 2025-03-24T1" in result.readable_output


@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        (("2023-01-01T12:00:00Z", "date"), datetime(2023, 1, 1, 12, 0, tzinfo=UTC)),
        ((datetime(2023, 1, 1, 12, 0), "date"), datetime(2023, 1, 1, 12, 0)),
        ((datetime(2023, 1, 1, 12, 0), "str"), "2023-01-01T12:00:00Z"),
        (("2023-01-01T12:00:00Z", "str"), "2023-01-01T12:00:00Z"),
    ],
    ids=["date_str_to_datetime", "date_datetime_pass", "datetime_to_iso_str", "iso_str_pass"],
)
def test_parse_date(test_input, expected_output):
    """
    Given:
        - A tuple input specifying the format type ('date' or 'isoformat') and the date value.
    When:
        - The parse_date function is invoked with the given format type and date.
    Then:
        - Confirm that the output matches the expected datetime object or ISO formatted string.
    """
    from GmailSingleUser import Client, parse_date

    client = Client()
    result = parse_date(client, *test_input)
    assert result == expected_output, f"Expected {expected_output} but got {result}"


@pytest.mark.parametrize(
    "input_args, expected",
    [
        (
            {"before": "now", "after": "1 day ago"},
            ("1672574400", "1672488000"),
        ),  # Unix timestamps for '2023-01-01 12:00:00', '2022-12-31 12:00:00'
        (
            {"before": "2023-01-01 12:00:00", "after": "2022-12-31 12:00:00"},
            ("1672574400", "1672488000"),
        ),  # Unix timestamps for the same fixed dates
        ({}, ("1672574400", "1672488000")),  # Default case: 'now' and '1 day ago' will be the same as the first case
    ],
    ids=["default_dates", "specific_dates", "no_params"],
)
@freeze_time("2023-01-01 12:00:00 UTC")
def test_get_unix_date(input_args, expected):
    """
    Given:
        - Dictionary with 'before' and 'after' date parameters, or defaults.
    When:
        - Converting dates to Unix timestamps using get_unix_date.
    Then:
        - Validate that the conversions match expected Unix timestamps.
    """
    from GmailSingleUser import get_unix_date

    assert get_unix_date(input_args) == expected


def test_fetch_incidents_set_last_run_called_correctly(mocker):
    """
    Test the `fetch_incidents` function to ensure it correctly updates the 'setLastRun' after fetching incidents.

    Given:
    - A GmailSingleUser client with a mocked Gmail service and necessary method patches.
    - A patch on 'GmailSingleUser.execute_gmail_action' to simulate Gmail API responses for 'list' and 'get' actions based on the fetch count.
    - Initial 'getLastRun' returning a specific datetime indicating the last run time.
    - An external file 'test_data/a.json' to simulate fetched email details.

    When:
    - The `fetch_incidents` function is called twice, first with an initial fetch count of 0, then incremented to simulate a subsequent fetch.

    Then:
    - The function should update 'setLastRun' correctly after each call:
      - After the first call, 'setLastRun' should contain exactly two messages with IDs '1' and '3'.
      - After the second call, 'setLastRun' should contain exactly three messages with IDs '1', '2', and '3', reflecting the updated fetch.
      - The 'lookback_msg' field in the data sent to 'setLastRun' should reflect the correct IDs of the messages fetched in each incident fetch.
    """  # noqa: E501
    from GmailSingleUser import fetch_incidents
    import GmailSingleUser

    gmail_single_user_client = Client()
    # Mock the chain of calls: service.users().messages().send().execute()
    mock_execute = mocker.Mock(return_value={"id": "mock_message_id"})
    mock_send = mocker.Mock(return_value=mock_execute)
    mock_messages = mocker.Mock(send=mocker.Mock(return_value=mock_send))
    mock_users = mocker.Mock(messages=mocker.Mock(return_value=mock_messages))
    mock_service = mocker.Mock(users=mocker.Mock(return_value=mock_users))
    # Patch the service object in the Client class to use the mocked service
    mocker.patch.object(GmailSingleUser.Client, "get_service", new=mock_service)
    fetch_count = 0

    def execute_gmail_action_side_effect(service, action, args):
        nonlocal fetch_count
        if action == "list":
            if fetch_count == 0:
                return {"messages": [{"id": "1"}, {"id": "3"}]}
            return {"messages": [{"id": "1"}, {"id": "2"}, {"id": "3"}]}
        elif action == "get":
            message_id = args.get("id")
            for item in util_load_json("test_data/lookback_emails.json"):
                if item.get("id") == message_id:
                    return item
        return {}

    # Mocking external functions and globals
    mocker.patch("GmailSingleUser.execute_gmail_action", side_effect=execute_gmail_action_side_effect)
    mocker.patch("GmailSingleUser.demisto.getLastRun", return_value={"gmt_time": "2025-02-24T15:17:05Z"})
    mock_set_last_run = mocker.patch("GmailSingleUser.demisto.setLastRun")

    fetch_incidents(gmail_single_user_client)

    last_args = mock_set_last_run.call_args[0][0]
    assert len(last_args["lookback_msg"]) == 2, "lookback_msg does not contain exactly three messages in the first fetch"

    fetch_count += 1
    fetch_incidents(gmail_single_user_client)

    # Check the last arguments sent to setLastRun
    last_args = mock_set_last_run.call_args[0][0]

    # Assert the lookback_msg contains the correct messages
    assert "lookback_msg" in last_args, "lookback_msg key is missing in the arguments sent to setLastRun"
    assert len(last_args["lookback_msg"]) == 3, "lookback_msg does not contain exactly three messages in the secund fetch"
    assert [id for id, _ in last_args["lookback_msg"]] == ["1", "2", "3"], "lookback_msg do"