selenium-outlook-mail-notifier 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- selenium_outlook_mail_notifier-0.1.0/LICENSE +21 -0
- selenium_outlook_mail_notifier-0.1.0/PKG-INFO +62 -0
- selenium_outlook_mail_notifier-0.1.0/README.md +49 -0
- selenium_outlook_mail_notifier-0.1.0/pyproject.toml +24 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier/__init__.py +4 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier/exceptions.py +11 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier/mailer.py +141 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/PKG-INFO +62 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/SOURCES.txt +13 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/dependency_links.txt +1 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/requires.txt +4 -0
- selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/top_level.txt +1 -0
- selenium_outlook_mail_notifier-0.1.0/setup.cfg +4 -0
- selenium_outlook_mail_notifier-0.1.0/tests/test_exceptions.py +30 -0
- selenium_outlook_mail_notifier-0.1.0/tests/test_mailer.py +184 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maschior
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: selenium-outlook-mail-notifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Selenium-based Outlook email notifier
|
|
5
|
+
Project-URL: Homepage, https://github.com/Maschior/selenium-outlook-mail-notifier
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: selenium>=4.49.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# selenium-outlook-mail-notifier
|
|
15
|
+
|
|
16
|
+
[](https://github.com/Maschior/selenium-outlook-mail-notifier/actions/workflows/tests.yml)
|
|
17
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
18
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
|
|
21
|
+
Send an email through Outlook Web (outlook.office.com) by driving a real
|
|
22
|
+
browser session with Selenium — useful when you don't have Microsoft Graph
|
|
23
|
+
API access but still need to trigger email notifications from a script.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install selenium-outlook-mail-notifier
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Chrome and a matching chromedriver available on PATH (or managed by
|
|
32
|
+
Selenium Manager, which ships with Selenium 4.6+).
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from selenium_outlook_mail_notifier import notify
|
|
38
|
+
|
|
39
|
+
notify(
|
|
40
|
+
subject="Something happened",
|
|
41
|
+
body="Body message",
|
|
42
|
+
recipient="someone@example.com", # optional, falls back to OUTLOOK_RECIPIENT
|
|
43
|
+
cc=["other@example.com"], # optional, falls back to OUTLOOK_CC
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Credentials are never passed as literals in code. `notify()` reads them from
|
|
48
|
+
environment variables unless you pass them explicitly:
|
|
49
|
+
|
|
50
|
+
| Variable | Required | Description |
|
|
51
|
+
|---------------------|----------|---------------------------------------|
|
|
52
|
+
| `OUTLOOK_EMAIL` | yes | Outlook account used to send the mail |
|
|
53
|
+
| `OUTLOOK_PASSWORD` | yes | Password for that account |
|
|
54
|
+
| `OUTLOOK_RECIPIENT` | no | Default recipient if not passed |
|
|
55
|
+
| `OUTLOOK_CC` | no | `;`-separated list of CC addresses |
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[test]"
|
|
61
|
+
pytest
|
|
62
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# selenium-outlook-mail-notifier
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Maschior/selenium-outlook-mail-notifier/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
5
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Send an email through Outlook Web (outlook.office.com) by driving a real
|
|
9
|
+
browser session with Selenium — useful when you don't have Microsoft Graph
|
|
10
|
+
API access but still need to trigger email notifications from a script.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install selenium-outlook-mail-notifier
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Chrome and a matching chromedriver available on PATH (or managed by
|
|
19
|
+
Selenium Manager, which ships with Selenium 4.6+).
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from selenium_outlook_mail_notifier import notify
|
|
25
|
+
|
|
26
|
+
notify(
|
|
27
|
+
subject="Something happened",
|
|
28
|
+
body="Body message",
|
|
29
|
+
recipient="someone@example.com", # optional, falls back to OUTLOOK_RECIPIENT
|
|
30
|
+
cc=["other@example.com"], # optional, falls back to OUTLOOK_CC
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Credentials are never passed as literals in code. `notify()` reads them from
|
|
35
|
+
environment variables unless you pass them explicitly:
|
|
36
|
+
|
|
37
|
+
| Variable | Required | Description |
|
|
38
|
+
|---------------------|----------|---------------------------------------|
|
|
39
|
+
| `OUTLOOK_EMAIL` | yes | Outlook account used to send the mail |
|
|
40
|
+
| `OUTLOOK_PASSWORD` | yes | Password for that account |
|
|
41
|
+
| `OUTLOOK_RECIPIENT` | no | Default recipient if not passed |
|
|
42
|
+
| `OUTLOOK_CC` | no | `;`-separated list of CC addresses |
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install -e ".[test]"
|
|
48
|
+
pytest
|
|
49
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "selenium-outlook-mail-notifier"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Selenium-based Outlook email notifier"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"selenium>=4.49.0",
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
test = [
|
|
13
|
+
"pytest>=8.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Maschior/selenium-outlook-mail-notifier"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools>=68"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
include = ["selenium_outlook_mail_notifier*"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
LOGGER = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ElementNotFoundError(Exception):
|
|
7
|
+
"""Raised when a Selenium element is not found in the DOM."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "Element not found"):
|
|
10
|
+
LOGGER.error(message)
|
|
11
|
+
super().__init__(message)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from selenium import webdriver
|
|
6
|
+
from selenium.webdriver.common.by import By
|
|
7
|
+
from selenium.common.exceptions import NoSuchElementException
|
|
8
|
+
|
|
9
|
+
from .exceptions import ElementNotFoundError
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _input_email(driver: webdriver.Chrome, email: str) -> None:
|
|
15
|
+
LOGGER.info("Attempting to input email.")
|
|
16
|
+
try:
|
|
17
|
+
driver.find_element("id", "i0116").send_keys(email)
|
|
18
|
+
driver.find_element("id", "idSIButton9").click()
|
|
19
|
+
time.sleep(3)
|
|
20
|
+
except NoSuchElementException as exc:
|
|
21
|
+
driver.quit()
|
|
22
|
+
raise ElementNotFoundError(
|
|
23
|
+
"Email input field not found (id='i0116' or button id='idSIButton9')."
|
|
24
|
+
) from exc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _input_password(driver: webdriver.Chrome, password: str) -> None:
|
|
28
|
+
LOGGER.info("Attempting to input password.")
|
|
29
|
+
try:
|
|
30
|
+
driver.find_element("id", "i0118").send_keys(password)
|
|
31
|
+
driver.find_element("id", "idSIButton9").click()
|
|
32
|
+
time.sleep(3)
|
|
33
|
+
except NoSuchElementException as exc:
|
|
34
|
+
driver.quit()
|
|
35
|
+
raise ElementNotFoundError(
|
|
36
|
+
"Password input field not found (id='i0118' or button id='idSIButton9')."
|
|
37
|
+
) from exc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _open_new_mail_window(driver: webdriver.Chrome) -> None:
|
|
41
|
+
LOGGER.info("Attempting to open new mail window.")
|
|
42
|
+
try:
|
|
43
|
+
driver.find_element(By.XPATH, "//button[@label='New mail']").click()
|
|
44
|
+
time.sleep(3)
|
|
45
|
+
except NoSuchElementException as exc:
|
|
46
|
+
driver.quit()
|
|
47
|
+
raise ElementNotFoundError(
|
|
48
|
+
"New mail button not found (XPATH \"//button[@label='New mail']\")."
|
|
49
|
+
) from exc
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fill_and_send(
|
|
53
|
+
driver: webdriver.Chrome,
|
|
54
|
+
recipient: str,
|
|
55
|
+
subject: str,
|
|
56
|
+
body_text: str,
|
|
57
|
+
cc: list[str],
|
|
58
|
+
) -> None:
|
|
59
|
+
LOGGER.info("Attempting to send email.")
|
|
60
|
+
try:
|
|
61
|
+
driver.find_element(By.XPATH, "//div[@aria-label='To']").send_keys(recipient)
|
|
62
|
+
except NoSuchElementException as exc:
|
|
63
|
+
driver.quit()
|
|
64
|
+
raise ElementNotFoundError(
|
|
65
|
+
"Recipient input field not found (XPATH \"//div[@aria-label='To']\")."
|
|
66
|
+
) from exc
|
|
67
|
+
|
|
68
|
+
if cc:
|
|
69
|
+
try:
|
|
70
|
+
driver.find_element(By.XPATH, "//div[@aria-label='Cc']").send_keys(";".join(cc))
|
|
71
|
+
except NoSuchElementException as exc:
|
|
72
|
+
driver.quit()
|
|
73
|
+
raise ElementNotFoundError(
|
|
74
|
+
"CC input field not found (XPATH \"//div[@aria-label='Cc']\")."
|
|
75
|
+
) from exc
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
driver.find_element(By.XPATH, "//input[@aria-label='Subject']").send_keys(subject)
|
|
79
|
+
except NoSuchElementException as exc:
|
|
80
|
+
driver.quit()
|
|
81
|
+
raise ElementNotFoundError(
|
|
82
|
+
"Subject input field not found (XPATH \"//input[@aria-label='Subject']\")."
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
driver.find_element(By.XPATH, "//div[@aria-label='Message body']").send_keys(body_text)
|
|
87
|
+
except NoSuchElementException as exc:
|
|
88
|
+
driver.quit()
|
|
89
|
+
raise ElementNotFoundError(
|
|
90
|
+
"Body input field not found (XPATH \"//div[@aria-label='Message body']\")."
|
|
91
|
+
) from exc
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
driver.find_element(By.XPATH, "//button[@aria-label='Send']").click()
|
|
95
|
+
except NoSuchElementException as exc:
|
|
96
|
+
driver.quit()
|
|
97
|
+
raise ElementNotFoundError(
|
|
98
|
+
"Send button not found (XPATH \"//button[@aria-label='Send']\")."
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def notify(
|
|
103
|
+
subject: str,
|
|
104
|
+
body: str,
|
|
105
|
+
recipient: str | None = None,
|
|
106
|
+
cc: list[str] | None = None,
|
|
107
|
+
sender_email: str | None = None,
|
|
108
|
+
sender_password: str | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Log in to Outlook Web via Selenium and send one notification email.
|
|
111
|
+
|
|
112
|
+
Credentials/recipient fall back to the OUTLOOK_EMAIL, OUTLOOK_PASSWORD,
|
|
113
|
+
OUTLOOK_RECIPIENT and OUTLOOK_CC environment variables when not passed
|
|
114
|
+
explicitly, so callers never need to hardcode secrets.
|
|
115
|
+
"""
|
|
116
|
+
sender_email = sender_email or os.environ["OUTLOOK_EMAIL"]
|
|
117
|
+
sender_password = sender_password or os.environ["OUTLOOK_PASSWORD"]
|
|
118
|
+
recipient = recipient or os.environ.get("OUTLOOK_RECIPIENT", sender_email)
|
|
119
|
+
if cc is None:
|
|
120
|
+
cc = [addr for addr in os.environ.get("OUTLOOK_CC", "").split(";") if addr]
|
|
121
|
+
|
|
122
|
+
start = time.time()
|
|
123
|
+
driver = webdriver.Chrome()
|
|
124
|
+
try:
|
|
125
|
+
LOGGER.info("Navigating to Outlook login page...")
|
|
126
|
+
driver.get("https://aka.ms/outlook")
|
|
127
|
+
time.sleep(5)
|
|
128
|
+
|
|
129
|
+
_input_email(driver, sender_email)
|
|
130
|
+
_input_password(driver, sender_password)
|
|
131
|
+
time.sleep(5) # wait for login to complete and the inbox to load
|
|
132
|
+
|
|
133
|
+
_open_new_mail_window(driver)
|
|
134
|
+
_fill_and_send(driver, recipient, subject, body, cc)
|
|
135
|
+
time.sleep(5) # wait for the email to be sent
|
|
136
|
+
|
|
137
|
+
LOGGER.info("Email probably sent successfully (sent items not checked).")
|
|
138
|
+
finally:
|
|
139
|
+
driver.quit()
|
|
140
|
+
|
|
141
|
+
LOGGER.info("notify() completed in %.2f seconds.", time.time() - start)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: selenium-outlook-mail-notifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Selenium-based Outlook email notifier
|
|
5
|
+
Project-URL: Homepage, https://github.com/Maschior/selenium-outlook-mail-notifier
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: selenium>=4.49.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# selenium-outlook-mail-notifier
|
|
15
|
+
|
|
16
|
+
[](https://github.com/Maschior/selenium-outlook-mail-notifier/actions/workflows/tests.yml)
|
|
17
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
18
|
+
[](https://pypi.org/project/selenium-outlook-mail-notifier/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
|
|
21
|
+
Send an email through Outlook Web (outlook.office.com) by driving a real
|
|
22
|
+
browser session with Selenium — useful when you don't have Microsoft Graph
|
|
23
|
+
API access but still need to trigger email notifications from a script.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install selenium-outlook-mail-notifier
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Chrome and a matching chromedriver available on PATH (or managed by
|
|
32
|
+
Selenium Manager, which ships with Selenium 4.6+).
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from selenium_outlook_mail_notifier import notify
|
|
38
|
+
|
|
39
|
+
notify(
|
|
40
|
+
subject="Something happened",
|
|
41
|
+
body="Body message",
|
|
42
|
+
recipient="someone@example.com", # optional, falls back to OUTLOOK_RECIPIENT
|
|
43
|
+
cc=["other@example.com"], # optional, falls back to OUTLOOK_CC
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Credentials are never passed as literals in code. `notify()` reads them from
|
|
48
|
+
environment variables unless you pass them explicitly:
|
|
49
|
+
|
|
50
|
+
| Variable | Required | Description |
|
|
51
|
+
|---------------------|----------|---------------------------------------|
|
|
52
|
+
| `OUTLOOK_EMAIL` | yes | Outlook account used to send the mail |
|
|
53
|
+
| `OUTLOOK_PASSWORD` | yes | Password for that account |
|
|
54
|
+
| `OUTLOOK_RECIPIENT` | no | Default recipient if not passed |
|
|
55
|
+
| `OUTLOOK_CC` | no | `;`-separated list of CC addresses |
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[test]"
|
|
61
|
+
pytest
|
|
62
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
selenium_outlook_mail_notifier/__init__.py
|
|
5
|
+
selenium_outlook_mail_notifier/exceptions.py
|
|
6
|
+
selenium_outlook_mail_notifier/mailer.py
|
|
7
|
+
selenium_outlook_mail_notifier.egg-info/PKG-INFO
|
|
8
|
+
selenium_outlook_mail_notifier.egg-info/SOURCES.txt
|
|
9
|
+
selenium_outlook_mail_notifier.egg-info/dependency_links.txt
|
|
10
|
+
selenium_outlook_mail_notifier.egg-info/requires.txt
|
|
11
|
+
selenium_outlook_mail_notifier.egg-info/top_level.txt
|
|
12
|
+
tests/test_exceptions.py
|
|
13
|
+
tests/test_mailer.py
|
selenium_outlook_mail_notifier-0.1.0/selenium_outlook_mail_notifier.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
selenium_outlook_mail_notifier
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from selenium_outlook_mail_notifier import ElementNotFoundError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_default_message():
|
|
9
|
+
exc = ElementNotFoundError()
|
|
10
|
+
assert str(exc) == "Element not found"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_custom_message():
|
|
14
|
+
exc = ElementNotFoundError("Custom message")
|
|
15
|
+
assert str(exc) == "Custom message"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_is_exception():
|
|
19
|
+
assert issubclass(ElementNotFoundError, Exception)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_logs_error(caplog):
|
|
23
|
+
with caplog.at_level(logging.ERROR):
|
|
24
|
+
ElementNotFoundError("Boom")
|
|
25
|
+
assert "Boom" in caplog.text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_raisable():
|
|
29
|
+
with pytest.raises(ElementNotFoundError, match="oops"):
|
|
30
|
+
raise ElementNotFoundError("oops")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from selenium.common.exceptions import NoSuchElementException
|
|
5
|
+
|
|
6
|
+
from selenium_outlook_mail_notifier import ElementNotFoundError
|
|
7
|
+
from selenium_outlook_mail_notifier.mailer import (
|
|
8
|
+
_fill_and_send,
|
|
9
|
+
_input_email,
|
|
10
|
+
_input_password,
|
|
11
|
+
_open_new_mail_window,
|
|
12
|
+
notify,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def driver():
|
|
18
|
+
return MagicMock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture(autouse=True)
|
|
22
|
+
def no_sleep():
|
|
23
|
+
with patch("selenium_outlook_mail_notifier.mailer.time.sleep"):
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestInputEmail:
|
|
28
|
+
def test_sends_email_and_clicks_next(self, driver):
|
|
29
|
+
_input_email(driver, "user@example.com")
|
|
30
|
+
|
|
31
|
+
driver.find_element.assert_any_call("id", "i0116")
|
|
32
|
+
driver.find_element.assert_any_call("id", "idSIButton9")
|
|
33
|
+
driver.find_element.return_value.send_keys.assert_called_with("user@example.com")
|
|
34
|
+
|
|
35
|
+
def test_missing_field_quits_driver_and_raises(self, driver):
|
|
36
|
+
driver.find_element.side_effect = NoSuchElementException()
|
|
37
|
+
|
|
38
|
+
with pytest.raises(ElementNotFoundError):
|
|
39
|
+
_input_email(driver, "user@example.com")
|
|
40
|
+
|
|
41
|
+
driver.quit.assert_called_once()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestInputPassword:
|
|
45
|
+
def test_sends_password_and_clicks_next(self, driver):
|
|
46
|
+
_input_password(driver, "hunter2")
|
|
47
|
+
|
|
48
|
+
driver.find_element.assert_any_call("id", "i0118")
|
|
49
|
+
driver.find_element.return_value.send_keys.assert_called_with("hunter2")
|
|
50
|
+
|
|
51
|
+
def test_missing_field_quits_driver_and_raises(self, driver):
|
|
52
|
+
driver.find_element.side_effect = NoSuchElementException()
|
|
53
|
+
|
|
54
|
+
with pytest.raises(ElementNotFoundError):
|
|
55
|
+
_input_password(driver, "hunter2")
|
|
56
|
+
|
|
57
|
+
driver.quit.assert_called_once()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestOpenNewMailWindow:
|
|
61
|
+
def test_clicks_new_mail_button(self, driver):
|
|
62
|
+
_open_new_mail_window(driver)
|
|
63
|
+
|
|
64
|
+
driver.find_element.return_value.click.assert_called_once()
|
|
65
|
+
|
|
66
|
+
def test_missing_button_quits_driver_and_raises(self, driver):
|
|
67
|
+
driver.find_element.side_effect = NoSuchElementException()
|
|
68
|
+
|
|
69
|
+
with pytest.raises(ElementNotFoundError):
|
|
70
|
+
_open_new_mail_window(driver)
|
|
71
|
+
|
|
72
|
+
driver.quit.assert_called_once()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestFillAndSend:
|
|
76
|
+
def test_fills_all_fields_and_sends(self, driver):
|
|
77
|
+
_fill_and_send(driver, "to@example.com", "Subject", "Body", ["cc@example.com"])
|
|
78
|
+
|
|
79
|
+
elements = driver.find_element.return_value
|
|
80
|
+
elements.send_keys.assert_any_call("to@example.com")
|
|
81
|
+
elements.send_keys.assert_any_call("cc@example.com")
|
|
82
|
+
elements.send_keys.assert_any_call("Subject")
|
|
83
|
+
elements.send_keys.assert_any_call("Body")
|
|
84
|
+
elements.click.assert_called_once()
|
|
85
|
+
|
|
86
|
+
def test_skips_cc_when_empty(self, driver):
|
|
87
|
+
_fill_and_send(driver, "to@example.com", "Subject", "Body", [])
|
|
88
|
+
|
|
89
|
+
calls = [c.args[0] for c in driver.find_element.call_args_list]
|
|
90
|
+
assert "//div[@aria-label='Cc']" not in calls
|
|
91
|
+
|
|
92
|
+
def test_missing_recipient_field_quits_driver_and_raises(self, driver):
|
|
93
|
+
driver.find_element.side_effect = NoSuchElementException()
|
|
94
|
+
|
|
95
|
+
with pytest.raises(ElementNotFoundError):
|
|
96
|
+
_fill_and_send(driver, "to@example.com", "Subject", "Body", [])
|
|
97
|
+
|
|
98
|
+
driver.quit.assert_called_once()
|
|
99
|
+
|
|
100
|
+
def test_missing_send_button_quits_driver_and_raises(self, driver):
|
|
101
|
+
ok = MagicMock()
|
|
102
|
+
driver.find_element.side_effect = [ok, ok, ok, NoSuchElementException()]
|
|
103
|
+
|
|
104
|
+
with pytest.raises(ElementNotFoundError):
|
|
105
|
+
_fill_and_send(driver, "to@example.com", "Subject", "Body", [])
|
|
106
|
+
|
|
107
|
+
driver.quit.assert_called_once()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestNotify:
|
|
111
|
+
@pytest.fixture(autouse=True)
|
|
112
|
+
def mocked_chrome(self, driver):
|
|
113
|
+
with patch("selenium_outlook_mail_notifier.mailer.webdriver.Chrome", return_value=driver):
|
|
114
|
+
yield driver
|
|
115
|
+
|
|
116
|
+
def test_uses_explicit_credentials_over_env(self, mocked_chrome, monkeypatch):
|
|
117
|
+
monkeypatch.setenv("OUTLOOK_EMAIL", "env@example.com")
|
|
118
|
+
monkeypatch.setenv("OUTLOOK_PASSWORD", "env-pass")
|
|
119
|
+
|
|
120
|
+
with patch("selenium_outlook_mail_notifier.mailer._input_email") as input_email, \
|
|
121
|
+
patch("selenium_outlook_mail_notifier.mailer._input_password") as input_password, \
|
|
122
|
+
patch("selenium_outlook_mail_notifier.mailer._open_new_mail_window"), \
|
|
123
|
+
patch("selenium_outlook_mail_notifier.mailer._fill_and_send"):
|
|
124
|
+
notify(
|
|
125
|
+
subject="Subject",
|
|
126
|
+
body="Body",
|
|
127
|
+
recipient="to@example.com",
|
|
128
|
+
sender_email="explicit@example.com",
|
|
129
|
+
sender_password="explicit-pass",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
input_email.assert_called_once_with(mocked_chrome, "explicit@example.com")
|
|
133
|
+
input_password.assert_called_once_with(mocked_chrome, "explicit-pass")
|
|
134
|
+
|
|
135
|
+
def test_falls_back_to_env_credentials(self, mocked_chrome, monkeypatch):
|
|
136
|
+
monkeypatch.setenv("OUTLOOK_EMAIL", "env@example.com")
|
|
137
|
+
monkeypatch.setenv("OUTLOOK_PASSWORD", "env-pass")
|
|
138
|
+
monkeypatch.delenv("OUTLOOK_RECIPIENT", raising=False)
|
|
139
|
+
monkeypatch.delenv("OUTLOOK_CC", raising=False)
|
|
140
|
+
|
|
141
|
+
with patch("selenium_outlook_mail_notifier.mailer._input_email") as input_email, \
|
|
142
|
+
patch("selenium_outlook_mail_notifier.mailer._input_password") as input_password, \
|
|
143
|
+
patch("selenium_outlook_mail_notifier.mailer._open_new_mail_window"), \
|
|
144
|
+
patch("selenium_outlook_mail_notifier.mailer._fill_and_send") as fill_and_send:
|
|
145
|
+
notify(subject="Subject", body="Body")
|
|
146
|
+
|
|
147
|
+
input_email.assert_called_once_with(mocked_chrome, "env@example.com")
|
|
148
|
+
input_password.assert_called_once_with(mocked_chrome, "env-pass")
|
|
149
|
+
fill_and_send.assert_called_once_with(mocked_chrome, "env@example.com", "Subject", "Body", [])
|
|
150
|
+
|
|
151
|
+
def test_missing_email_env_raises_key_error(self, mocked_chrome, monkeypatch):
|
|
152
|
+
monkeypatch.delenv("OUTLOOK_EMAIL", raising=False)
|
|
153
|
+
monkeypatch.delenv("OUTLOOK_PASSWORD", raising=False)
|
|
154
|
+
|
|
155
|
+
with pytest.raises(KeyError):
|
|
156
|
+
notify(subject="Subject", body="Body")
|
|
157
|
+
|
|
158
|
+
def test_parses_cc_from_env(self, mocked_chrome, monkeypatch):
|
|
159
|
+
monkeypatch.setenv("OUTLOOK_EMAIL", "env@example.com")
|
|
160
|
+
monkeypatch.setenv("OUTLOOK_PASSWORD", "env-pass")
|
|
161
|
+
monkeypatch.setenv("OUTLOOK_CC", "a@example.com;b@example.com")
|
|
162
|
+
|
|
163
|
+
with patch("selenium_outlook_mail_notifier.mailer._input_email"), \
|
|
164
|
+
patch("selenium_outlook_mail_notifier.mailer._input_password"), \
|
|
165
|
+
patch("selenium_outlook_mail_notifier.mailer._open_new_mail_window"), \
|
|
166
|
+
patch("selenium_outlook_mail_notifier.mailer._fill_and_send") as fill_and_send:
|
|
167
|
+
notify(subject="Subject", body="Body", recipient="to@example.com")
|
|
168
|
+
|
|
169
|
+
fill_and_send.assert_called_once_with(
|
|
170
|
+
mocked_chrome, "to@example.com", "Subject", "Body", ["a@example.com", "b@example.com"]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def test_quits_driver_even_when_step_fails(self, mocked_chrome, monkeypatch):
|
|
174
|
+
monkeypatch.setenv("OUTLOOK_EMAIL", "env@example.com")
|
|
175
|
+
monkeypatch.setenv("OUTLOOK_PASSWORD", "env-pass")
|
|
176
|
+
|
|
177
|
+
with patch(
|
|
178
|
+
"selenium_outlook_mail_notifier.mailer._input_email",
|
|
179
|
+
side_effect=ElementNotFoundError("boom"),
|
|
180
|
+
):
|
|
181
|
+
with pytest.raises(ElementNotFoundError):
|
|
182
|
+
notify(subject="Subject", body="Body", recipient="to@example.com")
|
|
183
|
+
|
|
184
|
+
mocked_chrome.quit.assert_called_once()
|