Skip to content

Commit 7c113a9

Browse files
don't terminate on notification (#1197)
1 parent a1fe18e commit 7c113a9

File tree

4 files changed

+62
-120
lines changed

4 files changed

+62
-120
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- fix: smtp & script notifications terminate the program [#898](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/898)
6+
57
## 1.29.0 (2025-07-19)
68

79
- fix: connection errors reported with stack trace [#1187](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1187)

src/icloudpd/authentication.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@
1515
from pyicloud_ipd.raw_policy import RawTreatmentPolicy
1616

1717

18-
class TwoStepAuthRequiredError(Exception):
19-
"""
20-
Raised when 2SA is required. base.py catches this exception
21-
and sends an email notification.
22-
"""
23-
24-
2518
def authenticator(
2619
logger: logging.Logger,
2720
domain: str,
@@ -33,8 +26,8 @@ def authenticator(
3326
mfa_provider: MFAProvider,
3427
status_exchange: StatusExchange,
3528
username: str,
29+
notificator: Callable[[], None],
3630
cookie_directory: str | None = None,
37-
raise_error_on_2sa: bool = False,
3831
client_id: str | None = None,
3932
) -> PyiCloudService:
4033
"""Authenticate with iCloud username and password"""
@@ -72,18 +65,16 @@ def password_provider(username: str, valid_password: List[str]) -> str | None:
7265
writer(username, valid_password[0])
7366

7467
if icloud.requires_2fa:
75-
if raise_error_on_2sa:
76-
raise TwoStepAuthRequiredError("Two-factor authentication is required")
7768
logger.info("Two-factor authentication is required (2fa)")
69+
notificator()
7870
if mfa_provider == MFAProvider.WEBUI:
7971
request_2fa_web(icloud, logger, status_exchange)
8072
else:
8173
request_2fa(icloud, logger)
8274

8375
elif icloud.requires_2sa:
84-
if raise_error_on_2sa:
85-
raise TwoStepAuthRequiredError("Two-step authentication is required")
8676
logger.info("Two-step authentication is required (2sa)")
77+
notificator()
8778
request_2sa(icloud, logger)
8879

8980
return icloud

src/icloudpd/base.py

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from tzlocal import get_localzone
4444

4545
from icloudpd import constants, download, exif_datetime
46-
from icloudpd.authentication import TwoStepAuthRequiredError, authenticator
46+
from icloudpd.authentication import authenticator
4747
from icloudpd.autodelete import autodelete_photos
4848
from icloudpd.config import Config
4949
from icloudpd.counter import Counter
@@ -837,9 +837,23 @@ def main(
837837
if directory is not None
838838
else (lambda _s, _c, _p: False)
839839
)
840+
notificator = partial(
841+
notificator_builder,
842+
logger,
843+
username,
844+
smtp_username,
845+
smtp_password,
846+
smtp_host,
847+
smtp_port,
848+
smtp_no_tls,
849+
notification_email,
850+
notification_email_from,
851+
notification_script,
852+
)
840853
result = core(
841854
passer,
842855
downloader,
856+
notificator,
843857
directory,
844858
username,
845859
auth_only,
@@ -855,15 +869,7 @@ def main(
855869
auto_delete,
856870
only_print_filenames,
857871
folder_structure,
858-
smtp_username,
859-
smtp_password,
860-
smtp_host,
861-
smtp_port,
862-
smtp_no_tls,
863-
notification_email,
864-
notification_email_from,
865872
no_progress_bar,
866-
notification_script,
867873
delete_after_download,
868874
keep_icloud_recent_days,
869875
domain,
@@ -881,6 +887,43 @@ def main(
881887
sys.exit(result)
882888

883889

890+
def notificator_builder(
891+
logger: logging.Logger,
892+
username: str,
893+
smtp_username: str | None,
894+
smtp_password: str | None,
895+
smtp_host: str,
896+
smtp_port: int,
897+
smtp_no_tls: bool,
898+
notification_email: str | None,
899+
notification_email_from: str | None,
900+
notification_script: str | None,
901+
) -> None:
902+
try:
903+
if notification_script is not None:
904+
logger.debug("Executing notification script...")
905+
subprocess.call([notification_script])
906+
else:
907+
pass
908+
if smtp_username is not None or notification_email is not None:
909+
send_2sa_notification(
910+
logger,
911+
username,
912+
smtp_username,
913+
smtp_password,
914+
smtp_host,
915+
smtp_port,
916+
smtp_no_tls,
917+
notification_email,
918+
notification_email_from,
919+
)
920+
else:
921+
pass
922+
except Exception as error:
923+
logger.error("Notification of the required MFA failed")
924+
logger.debug(error)
925+
926+
884927
def where_builder(
885928
logger: logging.Logger,
886929
skip_videos: bool,
@@ -1239,6 +1282,7 @@ def composed(ex: Exception, retries: int) -> None:
12391282
def core(
12401283
passer: Callable[[PhotoAsset], bool],
12411284
downloader: Callable[[PyiCloudService, Counter, PhotoAsset], bool],
1285+
notificator: Callable[[], None],
12421286
directory: str | None,
12431287
username: str,
12441288
auth_only: bool,
@@ -1254,15 +1298,7 @@ def core(
12541298
auto_delete: bool,
12551299
only_print_filenames: bool,
12561300
folder_structure: str,
1257-
smtp_username: str | None,
1258-
smtp_password: str | None,
1259-
smtp_host: str,
1260-
smtp_port: int,
1261-
smtp_no_tls: bool,
1262-
notification_email: str | None,
1263-
notification_email_from: str | None,
12641301
no_progress_bar: bool,
1265-
notification_script: str | None,
12661302
delete_after_download: bool,
12671303
keep_icloud_recent_days: int | None,
12681304
domain: str,
@@ -1282,11 +1318,6 @@ def core(
12821318
skip_bar = not os.environ.get("FORCE_TQDM") and (
12831319
only_print_filenames or no_progress_bar or not sys.stdout.isatty()
12841320
)
1285-
raise_error_on_2sa = (
1286-
smtp_username is not None
1287-
or notification_email is not None
1288-
or notification_script is not None
1289-
)
12901321
while True: # watch with interval
12911322
try:
12921323
icloud = authenticator(
@@ -1300,8 +1331,8 @@ def core(
13001331
mfa_provider,
13011332
status_exchange,
13021333
username,
1334+
notificator,
13031335
cookie_directory,
1304-
raise_error_on_2sa,
13051336
os.environ.get("CLIENT_ID"),
13061337
)
13071338

@@ -1518,26 +1549,6 @@ def should_break(counter: Counter) -> bool:
15181549
return 1
15191550
else:
15201551
pass
1521-
except TwoStepAuthRequiredError:
1522-
if notification_script is not None:
1523-
subprocess.call([notification_script])
1524-
else:
1525-
pass
1526-
if smtp_username is not None or notification_email is not None:
1527-
send_2sa_notification(
1528-
logger,
1529-
username,
1530-
smtp_username,
1531-
smtp_password,
1532-
smtp_host,
1533-
smtp_port,
1534-
smtp_no_tls,
1535-
notification_email,
1536-
notification_email_from,
1537-
)
1538-
else:
1539-
pass
1540-
return 1
15411552
if watch_interval: # pragma: no cover
15421553
logger.info(f"Waiting for {watch_interval} sec...")
15431554
interval: Sequence[int] = range(1, watch_interval)

tests/test_authentication.py

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# import vcr
1414
import pyicloud_ipd
1515
from foundation.core import constant, identity
16-
from icloudpd.authentication import TwoStepAuthRequiredError, authenticator
16+
from icloudpd.authentication import authenticator
1717
from icloudpd.base import dummy_password_writter, lp_filename_concatinator, main
1818
from icloudpd.logger import setup_logger
1919
from icloudpd.mfa_provider import MFAProvider
@@ -56,8 +56,8 @@ def test_failed_auth(self) -> None:
5656
MFAProvider.CONSOLE,
5757
StatusExchange(),
5858
"bad_username",
59+
lambda: None,
5960
cookie_dir,
60-
False,
6161
"EC5646DE-9423-11E8-BF21-14109FE0B321",
6262
)
6363

@@ -96,68 +96,6 @@ def test_fallback_raw_password(self) -> None:
9696
self.assertIn("INFO Authentication completed successfully", self._caplog.text)
9797
assert result.exit_code == 0
9898

99-
def test_2sa_required(self) -> None:
100-
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
101-
cookie_dir = os.path.join(base_dir, "cookie")
102-
103-
for dir in [base_dir, cookie_dir]:
104-
recreate_path(dir)
105-
106-
with vcr.use_cassette(os.path.join(self.vcr_path, "2sa_flow_valid_code.yml")):
107-
with self.assertRaises(TwoStepAuthRequiredError) as context:
108-
# To re-record this HTTP request,
109-
# delete ./tests/vcr_cassettes/auth_requires_2sa.yml,
110-
# put your actual credentials in here, run the test,
111-
# and then replace with dummy credentials.
112-
authenticator(
113-
setup_logger(),
114-
"com",
115-
identity,
116-
lp_filename_concatinator,
117-
RawTreatmentPolicy.AS_IS,
118-
FileMatchPolicy.NAME_SIZE_DEDUP_WITH_SUFFIX,
119-
{"test": (constant("dummy"), dummy_password_writter)},
120-
MFAProvider.CONSOLE,
121-
StatusExchange(),
122-
"jdoe@gmail.com",
123-
cookie_dir,
124-
True,
125-
"DE309E26-942E-11E8-92F5-14109FE0B321",
126-
)
127-
128-
self.assertTrue("Two-step authentication is required" in str(context.exception))
129-
130-
def test_2fa_required(self) -> None:
131-
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
132-
cookie_dir = os.path.join(base_dir, "cookie")
133-
134-
for dir in [base_dir, cookie_dir]:
135-
recreate_path(dir)
136-
137-
with vcr.use_cassette(os.path.join(self.vcr_path, "auth_requires_2fa.yml")):
138-
with self.assertRaises(TwoStepAuthRequiredError) as context:
139-
# To re-record this HTTP request,
140-
# delete ./tests/vcr_cassettes/auth_requires_2fa.yml,
141-
# put your actual credentials in here, run the test,
142-
# and then replace with dummy credentials.
143-
authenticator(
144-
setup_logger(),
145-
"com",
146-
identity,
147-
lp_filename_concatinator,
148-
RawTreatmentPolicy.AS_IS,
149-
FileMatchPolicy.NAME_SIZE_DEDUP_WITH_SUFFIX,
150-
{"test": (constant("dummy"), dummy_password_writter)},
151-
MFAProvider.CONSOLE,
152-
StatusExchange(),
153-
"jdoe@gmail.com",
154-
cookie_dir,
155-
True,
156-
"EC5646DE-9423-11E8-BF21-14109FE0B321",
157-
)
158-
159-
self.assertTrue("Two-factor authentication is required" in str(context.exception))
160-
16199
def test_successful_token_validation(self) -> None:
162100
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
163101
cookie_dir = os.path.join(base_dir, "cookie")
@@ -338,8 +276,8 @@ def test_non_2fa(self) -> None:
338276
MFAProvider.CONSOLE,
339277
StatusExchange(),
340278
"jdoe@gmail.com",
279+
lambda: None,
341280
cookie_dir,
342-
True,
343281
"EC5646DE-9423-11E8-BF21-14109FE0B321",
344282
)
345283

0 commit comments

Comments
 (0)