Repository: mandarons/icloud-drive-docker (python)

Issue:
[FEATURE] SMTP configurable Mail Subject
As a family administrator of multiple iCloud-drive-docker instances, I want to know which of my family members need to reauthenticate so that I can directly support the right family member with our searching trough all my container logs.

As a simple solution the subject of the mail for reauthication should be made configurable. As of now it's fixed "icloud-docker: Two step authentication required". This could be the default value of a new configuration option in the config YAML. With that option I could configure it for example this way: "icloud-docker: Two step authentication required for User XYZ"

As an alternative the content of the mail could be a little bit more dynamic, so that the correct username is already filled out into the body (via the already configured username in the yaml). With that I could simpler do a copy and paste of the command :) 

today:

```
Two-step authentication for iCloud Drive, Photos (Docker) is required.
Please login to your server and authenticate. Please run -
`docker exec -it --user=icd icloud /bin/sh -c "icloud --username=<icloud-username>
--session-directory=/app/session_data"`.

```

tomorrow:
```
Two-step authentication for iCloud Drive, Photos (Docker) is required.
Please login to your server and authenticate. Please run -
`docker exec -it --user=icd icloud /bin/sh -c "icloud --username=asdfa@icloud.com
--session-directory=/app/session_data"`.
```

Maybe both features are making sense... It's for sure up to you! :)


Pull request description:
[FEATURE] Added username to notifications
Fixes #217

Patch:
diff --git a/src/notify.py b/src/notify.py
index da655c2d..957ba0d8 100644
--- a/src/notify.py
+++ b/src/notify.py
@@ -7,13 +7,8 @@ import requests
 from src import LOGGER, config_parser
 from src.email_message import EmailMessage as Message
 
-MESSAGE_BODY = """Two-step authentication for iCloud Drive, Photos (Docker) is required.
-                Please login to your server and authenticate. Please run -
-                `docker exec -it --user=icd icloud /bin/sh -c "icloud --username=<icloud-username>
-                --session-directory=/app/session_data"`."""
-
 
-def notify_telegram(config, last_send=None, dry_run=False):
+def notify_telegram(config, message, last_send=None, dry_run=False):
     """Send telegram notification."""
     sent_on = None
     bot_token = config_parser.get_telegram_bot_token(config=config)
@@ -29,7 +24,7 @@ def notify_telegram(config, last_send=None, dry_run=False):
             if not post_message_to_telegram(
                 bot_token,
                 chat_id,
-                MESSAGE_BODY,
+                message,
             ):
                 sent_on = None
     else:
@@ -51,18 +46,18 @@ def post_message_to_telegram(bot_token, chat_id, message):
     return False
 
 
-def post_message_to_discord(webhook_url, username):
+def post_message_to_discord(webhook_url, username, message):
     """Post message to discord webhook."""
-    data = {"username": username, "content": MESSAGE_BODY}
+    data = {"username": username, "content": message}
     response = requests.post(webhook_url, data=data, timeout=10)
     if response.status_code == 204:
         return True
     # Log error message
-    LOGGER.error(f"Failed to send telegram notification. Response: {response.text}")
+    LOGGER.error(f"Failed to send Discord notification. Response: {response.text}")
     return False
 
 
-def notify_discord(config, last_send=None, dry_run=False):
+def notify_discord(config, message, last_send=None, dry_run=False):
     """Send discord notification."""
     sent_on = None
     webhook_url = config_parser.get_discord_webhook_url(config=config)
@@ -75,7 +70,7 @@ def notify_discord(config, last_send=None, dry_run=False):
         sent_on = datetime.datetime.now()
         if not dry_run:
             # Post message to discord webhook using API
-            if not post_message_to_discord(webhook_url, username):
+            if not post_message_to_discord(webhook_url, username, message):
                 sent_on = None
     else:
         LOGGER.warning(
@@ -84,11 +79,18 @@ def notify_discord(config, last_send=None, dry_run=False):
     return sent_on
 
 
-def send(config, last_send=None, dry_run=False):
+def send(config, username, last_send=None, dry_run=False):
     """Send notifications."""
     sent_on = None
-    notify_telegram(config=config, last_send=last_send, dry_run=dry_run)
-    notify_discord(config=config, last_send=last_send, dry_run=dry_run)
+    message = f"""Two-step authentication for iCloud Drive, Photos (Docker) is required.
+                Please login to your server and authenticate. Please run -
+                `docker exec -it --user=icd icloud /bin/sh -c
+                "icloud --session-directory=/app/session_data --username={username}"`."""
+    subject = f"icloud-docker: Two step authentication is required for {username}"
+    notify_telegram(
+        config=config, message=message, last_send=last_send, dry_run=dry_run
+    )
+    notify_discord(config=config, message=message, last_send=last_send, dry_run=dry_run)
     email = config_parser.get_smtp_email(config=config)
     to_email = config_parser.get_smtp_to_email(config=config)
     host = config_parser.get_smtp_host(config=config)
@@ -116,7 +118,7 @@ def send(config, last_send=None, dry_run=False):
                     else:
                         smtp.login(email, password)
 
-                msg = build_message(email, to_email)
+                msg = build_message(email, to_email, message, subject)
 
                 smtp.sendmail(from_addr=email, to_addrs=to_email, msg=msg.as_string())
                 smtp.quit()
@@ -129,15 +131,12 @@ def send(config, last_send=None, dry_run=False):
     return sent_on
 
 
-def build_message(email, to_email):
+def build_message(email, to_email, message, subject):
     """Create email message."""
-    message = Message(to=to_email)
-    message.sender = "icloud-docker <" + email + ">"
-    message.date = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
-    message.subject = "icloud-docker: Two step authentication required"
-    message.body = """Two-step authentication for iCloud Drive, Photos (Docker) is required.
-Please login to your server and authenticate. Please run -
-`docker exec -it --user=icd icloud /bin/sh -c "icloud --username=<icloud-username>
---session-directory=/app/session_data"`."""
-
-    return message
+    msg = Message(to=to_email)
+    msg.sender = "icloud-docker <" + email + ">"
+    msg.date = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
+    msg.subject = subject
+    msg.body = message
+
+    return msg
diff --git a/src/sync.py b/src/sync.py
index 66b500b4..79a34ae6 100644
--- a/src/sync.py
+++ b/src/sync.py
@@ -104,7 +104,9 @@ def sync():
                         LOGGER.info("retry_login_interval is < 0, exiting ...")
                         break
                     LOGGER.info(f"Retrying login at {next_sync} ...")
-                    last_send = notify.send(config, last_send)
+                    last_send = notify.send(
+                        config=config, username=username, last_send=last_send
+                    )
                     sleep(sleep_for)
                     continue
             except exceptions.ICloudPyNoStoredPasswordAvailableException:
@@ -116,7 +118,9 @@ def sync():
                     datetime.datetime.now() + datetime.timedelta(seconds=sleep_for)
                 ).strftime("%c")
                 LOGGER.info(f"Retrying login at {next_sync} ...")
-                last_send = notify.send(config, last_send)
+                last_send = notify.send(
+                    config=config, username=username, last_send=last_send
+                )
                 sleep(sleep_for)
                 continue
 


Test patch:
diff --git a/tests/test_notify.py b/tests/test_notify.py
index 933b180e..d5f7d249 100644
--- a/tests/test_notify.py
+++ b/tests/test_notify.py
@@ -6,7 +6,6 @@ from unittest.mock import patch
 from src import config_parser, notify
 from src.email_message import EmailMessage as Message
 from src.notify import (
-    MESSAGE_BODY,
     notify_discord,
     notify_telegram,
     post_message_to_discord,
@@ -31,6 +30,7 @@ class TestNotify(unittest.TestCase):
                 "telegram": {"bot_token": "bot_token", "chat_id": "chat_id"},
             }
         }
+        self.message_body = "message body"
 
     def test_throttling(self):
         """Test for throttled notification."""
@@ -38,7 +38,8 @@ class TestNotify(unittest.TestCase):
         # if less than 24 hours has passed since last send, then the same
         # datetime object is returned
         self.assertEqual(
-            not_24_hours, notify.send(self.config, not_24_hours, dry_run=True)
+            not_24_hours,
+            notify.send(self.config, "username@icloud.com", not_24_hours, dry_run=True),
         )
 
     def test_no_smtp_config(self):
@@ -53,23 +54,31 @@ class TestNotify(unittest.TestCase):
 
     def test_build_message(self):
         """Test for building a valid email."""
+        subject = "icloud-docker: Two step authentication required"
+        message = (
+            "Two-step authentication for iCloud Drive, Photos (Docker) is required."
+        )
+
         msg = notify.build_message(
             email=self.config["app"]["smtp"]["email"],
             to_email=self.config["app"]["smtp"]["to"],
+            subject=subject,
+            message=message,
         )
         self.assertEqual(msg.to, self.config["app"]["smtp"]["to"])
         self.assertIn(self.config["app"]["smtp"]["email"], msg.sender)
-        self.assertIn("icloud-docker: Two step authentication required", msg.subject)
+        self.assertIn(subject, msg.subject)
         self.assertIn(
-            "Two-step authentication for iCloud Drive, Photos (Docker) is required.",
+            message,
             msg.body,
         )
         self.assertIsInstance(msg, Message)
 
     def test_send(self):
         """Test for email send."""
+        username = "username@icloud.com"
         with patch("smtplib.SMTP") as smtp:
-            notify.send(self.config)
+            notify.send(self.config, username=username)
 
             instance = smtp.return_value
 
@@ -89,15 +98,16 @@ class TestNotify(unittest.TestCase):
 
             # verify that the message was passed to sendmail()
             self.assertIn(
-                "Subject: icloud-docker: Two step authentication required",
+                "Subject: icloud-docker: Two step authentication is required",
                 instance.sendmail.mock_calls[0][2]["msg"],
             )
 
     def test_send_with_username(self):
         """Test for email send."""
+        username = "username@icloud.com"
         with patch("smtplib.SMTP") as smtp:
             self.config["app"]["smtp"]["username"] = "smtp-username"
-            notify.send(self.config)
+            notify.send(self.config, username)
 
             instance = smtp.return_value
 
@@ -117,17 +127,18 @@ class TestNotify(unittest.TestCase):
 
             # verify that the message was passed to sendmail()
             self.assertIn(
-                "Subject: icloud-docker: Two step authentication required",
+                "Subject: icloud-docker: Two step authentication is required",
                 instance.sendmail.mock_calls[0][2]["msg"],
             )
 
     def test_send_fail(self):
         """Test for failed send."""
+        username = "username@icloud.com"
         with patch("smtplib.SMTP") as smtp:
             smtp.side_effect = Exception
 
             # Verify that a failure doesn't return a send_on timestamp
-            sent_on = notify.send(self.config)
+            sent_on = notify.send(self.config, username)
             self.assertEqual(None, sent_on)
 
     def test_notify_telegram_success(self):
@@ -139,13 +150,13 @@ class TestNotify(unittest.TestCase):
         }
 
         with patch("src.notify.post_message_to_telegram") as post_message_mock:
-            notify_telegram(config, None, False)
+            notify_telegram(config, self.message_body, None, False)
 
             # Verify that post_message_to_telegram is called with the correct arguments
             post_message_mock.assert_called_once_with(
                 config["app"]["telegram"]["bot_token"],
                 config["app"]["telegram"]["chat_id"],
-                MESSAGE_BODY,
+                self.message_body,
             )
 
     def test_notify_telegram_fail(self):
@@ -158,13 +169,13 @@ class TestNotify(unittest.TestCase):
 
         with patch("src.notify.post_message_to_telegram") as post_message_mock:
             post_message_mock.return_value = False
-            notify_telegram(config, None, False)
+            notify_telegram(config, self.message_body, None, False)
 
             # Verify that post_message_to_telegram is called with the correct arguments
             post_message_mock.assert_called_once_with(
                 config["app"]["telegram"]["bot_token"],
                 config["app"]["telegram"]["chat_id"],
-                MESSAGE_BODY,
+                self.message_body,
             )
 
     def test_notify_telegram_throttling(self):
@@ -190,7 +201,7 @@ class TestNotify(unittest.TestCase):
         dry_run = True
 
         with patch("src.notify.post_message_to_telegram") as post_message_mock:
-            notify_telegram(config, last_send, dry_run)
+            notify_telegram(config, self.message_body, last_send, dry_run)
 
             # Verify that post_message_to_telegram is not called in dry run mode
             post_message_mock.assert_not_called()
@@ -240,12 +251,13 @@ class TestNotify(unittest.TestCase):
         }
 
         with patch("src.notify.post_message_to_discord") as post_message_mock:
-            notify_discord(config, None, False)
+            notify_discord(config, self.message_body, None, False)
 
             # Verify that post_message_to_discord is called with the correct arguments
             post_message_mock.assert_called_once_with(
                 config["app"]["discord"]["webhook_url"],
                 config["app"]["discord"]["username"],
+                self.message_body,
             )
             self.assertEqual(post_message_mock.call_count, 1)
 
@@ -257,12 +269,13 @@ class TestNotify(unittest.TestCase):
 
         with patch("src.notify.post_message_to_discord") as post_message_mock:
             post_message_mock.return_value = False
-            notify_discord(config, None, False)
+            notify_discord(config, self.message_body, None, False)
 
             # Verify that post_message_to_discord is called with the correct arguments
             post_message_mock.assert_called_once_with(
                 config["app"]["discord"]["webhook_url"],
                 config["app"]["discord"]["username"],
+                self.message_body,
             )
 
     def test_notify_discord_throttling(self):
@@ -274,7 +287,7 @@ class TestNotify(unittest.TestCase):
         dry_run = False
 
         with patch("src.notify.post_message_to_discord") as post_message_mock:
-            notify_discord(config, last_send, dry_run)
+            notify_discord(config, self.message_body, last_send, dry_run)
 
             # Verify that post_message_to_discord is not called when throttled
             post_message_mock.assert_not_called()
@@ -288,7 +301,7 @@ class TestNotify(unittest.TestCase):
         dry_run = True
 
         with patch("src.notify.post_message_to_discord") as post_message_mock:
-            notify_discord(config, last_send, dry_run)
+            notify_discord(config, self.message_body, last_send, dry_run)
 
             # Verify that post_message_to_discord is not called in dry run mode
             post_message_mock.assert_not_called()
@@ -307,26 +320,28 @@ class TestNotify(unittest.TestCase):
 
     def test_post_message_to_discord(self):
         """Test for successful post."""
+        message = "message"
         with patch("requests.post") as post_mock:
             post_mock.return_value.status_code = 204
-            post_message_to_discord("webhook_url", "username")
+            post_message_to_discord("webhook_url", "username", message)
 
             # Verify that post is called with the correct arguments
             post_mock.assert_called_once_with(
                 "webhook_url",
-                data={"content": MESSAGE_BODY, "username": "username"},
+                data={"content": message, "username": "username"},
                 timeout=10,
             )
 
     def test_post_message_to_discord_fail(self):
         """Test for failed post."""
+        message = "discord message"
         with patch("requests.post") as post_mock:
             post_mock.return_value.status_code = 400
-            post_message_to_discord("webhook_url", "username")
+            post_message_to_discord("webhook_url", "username", message)
 
             # Verify that post is called with the correct arguments
             post_mock.assert_called_once_with(
                 "webhook_url",
-                data={"content": MESSAGE_BODY, "username": "username"},
+                data={"content": message, "username": "username"},
                 timeout=10,
             )