From f0c748ebc354a243ba437e3e998eec3d2f69d181 Mon Sep 17 00:00:00 2001 From: firstof9 Date: Sat, 27 Mar 2021 15:12:06 -0700 Subject: [PATCH 1/2] Add amazon exception sensor --- custom_components/mail_and_packages/const.py | 6 + .../mail_and_packages/helpers.py | 48 + custom_components/mail_and_packages/sensor.py | 5 +- tests/conftest.py | 25 + tests/test_emails/amazon_exception.eml | 1196 +++++++++++++++++ tests/test_helpers.py | 15 + 6 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 tests/test_emails/amazon_exception.eml diff --git a/custom_components/mail_and_packages/const.py b/custom_components/mail_and_packages/const.py index 09bcae2f..9f34a326 100644 --- a/custom_components/mail_and_packages/const.py +++ b/custom_components/mail_and_packages/const.py @@ -69,6 +69,11 @@ AMAZON_HUB_EMAIL = "thehub@amazon.com" AMAZON_HUB_SUBJECT = "(You have a package to pick up)(.*)- (\\d{6})" AMAZON_TIME_PATTERN = "will arrive:,estimated delivery date is:,guaranteed delivery date is:,Arriving:,Arriver:" +AMAZON_EXCEPTION_SUBJECT = "Delivery update:" +AMAZON_EXCEPTION_BODY = "running late" +AMAZON_EXCEPTION = "amazon_exception" +AMAZON_EXCEPTION_ORDER = "amazon_exception_order" +AMAZON_PATTERN = "[0-9]{3}-[0-9]{7}-[0-9]{7}" # Sensor Data SENSOR_DATA = { @@ -220,6 +225,7 @@ "package(s)", "mdi:package-variant-closed", ], + "amazon_exception": ["Mail Amazon Exception", "package(s)", "mdi:archive-alert"], "amazon_hub": ["Mail Amazon Hub Packages", "package(s)", "mdi:amazon"], "capost_delivered": [ "Mail Canada Post Delivered", diff --git a/custom_components/mail_and_packages/helpers.py b/custom_components/mail_and_packages/helpers.py index d0009994..cf3b5adf 100644 --- a/custom_components/mail_and_packages/helpers.py +++ b/custom_components/mail_and_packages/helpers.py @@ -304,6 +304,10 @@ def fetch( value = amazon_hub(account, amazon_fwds) count[sensor] = value[const.ATTR_COUNT] count[const.AMAZON_HUB_CODE] = value[const.ATTR_CODE] + elif sensor == const.AMAZON_EXCEPTION: + info = amazon_exception(account, amazon_fwds) + count[sensor] = info[const.ATTR_COUNT] + count[const.AMAZON_EXCEPTION_ORDER] = info[const.ATTR_ORDER] elif "_packages" in sensor: prefix = sensor.split("_")[0] delivering = fetch(hass, config, account, data, f"{prefix}_delivering") @@ -1003,6 +1007,50 @@ def amazon_hub(account: Type[imaplib.IMAP4_SSL], fwds: Optional[str] = None) -> return info +def amazon_exception( + account: Type[imaplib.IMAP4_SSL], fwds: Optional[str] = None +) -> dict: + """Find Amazon exception emails. + + Returns dict of sensor data + """ + orderNum = [] + tfmt = get_formatted_date() + subject = const.AMAZON_EXCEPTION_SUBJECT + pattern = const.AMAZON_PATTERN + count = 0 + info = {} + domains = const.Amazon_Domains.split(",") + if isinstance(fwds, list): + for fwd in fwds: + if fwd != '""': + domains.append(fwd) + _LOGGER.debug("Amazon email adding %s to list", str(fwd)) + + for domain in domains: + if "@" in domain: + email_address = domain.strip('"') + _LOGGER.debug("Amazon email search address: %s", str(email_address)) + else: + email_address = [] + addresses = const.AMAZON_SHIPMENT_TRACKING + for address in addresses: + email_address.append(f"{address}@{domain}") + _LOGGER.debug("Amazon email search address: %s", str(email_address)) + + (rv, sdata) = email_search(account, email_address, tfmt, subject) + count += len(sdata[0].split()) + _LOGGER.debug("Found %s Amazon exceptions", count) + ordernums = get_tracking(sdata[0], account, pattern) + for order in ordernums: + orderNum.append(order) + + info[const.ATTR_COUNT] = count + info[const.ATTR_ORDER] = orderNum + + return info + + def get_items( account: Type[imaplib.IMAP4_SSL], param: str = None, diff --git a/custom_components/mail_and_packages/sensor.py b/custom_components/mail_and_packages/sensor.py index 1eff7c4d..4faefb80 100644 --- a/custom_components/mail_and_packages/sensor.py +++ b/custom_components/mail_and_packages/sensor.py @@ -92,7 +92,10 @@ def device_state_attributes(self) -> Optional[str]: data = self.coordinator.data if "Amazon" in self._name: - attr[const.ATTR_ORDER] = data[const.AMAZON_ORDER] + if self._name == "amazon_exception": + attr[const.ATTR_ORDER] = data[const.AMAZON_EXCEPTION_ORDER] + else: + attr[const.ATTR_ORDER] = data[const.AMAZON_ORDER] elif "Mail USPS Mail" == self._name: attr[const.ATTR_IMAGE] = data[const.ATTR_IMAGE_NAME] elif "_delivering" in self.type and tracking in self.data.keys(): diff --git a/tests/conftest.py b/tests/conftest.py index 2affd747..cc6131b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -978,3 +978,28 @@ def mock_copytree(): with patch("custom_components.mail_and_packages.helpers.copytree") as mock_copytree: mock_copytree.return_value = True yield mock_copytree + + +@pytest.fixture() +def mock_imap_amazon_exception(): + """ Mock imap class values. """ + with patch( + "custom_components.mail_and_packages.helpers.imaplib" + ) as mock_imap_amazon_exception: + mock_conn = mock.Mock(spec=imaplib.IMAP4_SSL) + mock_imap_amazon_exception.IMAP4_SSL.return_value = mock_conn + + mock_conn.login.return_value = ( + "OK", + [b"user@fake.email authenticated (Success)"], + ) + mock_conn.list.return_value = ( + "OK", + [b'(\\HasNoChildren) "/" "INBOX"'], + ) + mock_conn.search.return_value = ("OK", [b"1"]) + f = open("tests/test_emails/amazon_exception.eml", "r") + email_file = f.read() + mock_conn.fetch.return_value = ("OK", [(b"", email_file.encode("utf-8"))]) + mock_conn.select.return_value = ("OK", []) + yield mock_conn diff --git a/tests/test_emails/amazon_exception.eml b/tests/test_emails/amazon_exception.eml new file mode 100644 index 00000000..6fe0566f --- /dev/null +++ b/tests/test_emails/amazon_exception.eml @@ -0,0 +1,1196 @@ +From: "Amazon.com" +Reply-To: no-reply@amazon.com +To: fakeuser@fake.email +Subject: Delivery update: Logitech ERGO M575 Wireless... +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_9124858_607956531.1616799814029" +Date: Fri, 26 Mar 2021 23:03:34 +0000 + +------=_Part_9124858_607956531.1616799814029 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Amazon.com +---------------------------------------------------------------------------= +----------------------------------------------------------------- + +Hi FakeUser, + +Your package is on the way but running late. We=E2=80=99re sorry for the de= +lay. + +Now expected March 27 - March 28. Track your delivery for the latest update= +s. + +Track your delivery: +https://www.amazon.com/gp/css/shiptrack/view.html + +Order #123-1234567-1234567 + + Logitech ERGO M575 Wireless Trackball Mouse, Easy thumb control, Precis= +ion and smooth tracking, Ergonomic comfort design, Windows/Mac, Bluetooth, = +USB=20 + +---------------------------------------------------------------------------= +----------------------------------------------------------------- + +This email was sent from an email address that can't receive emails. Please= + don't reply to this email. +------=_Part_9124858_607956531.1616799814029 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + + + + =20 + =20 + =20 + =20 + =20 + =20 + =20 + =20 + =20 + + + + + + +
=20 + =20 + + =20 + =20 + =20 + +
=20 + =20 + + =20 + =20 + =20 + +
=20 + =20 + + =20 + =20 + =20 + +
=20 + 3D"Amazon.com"=20 +
=20 + =20 + + =20 + =20 + =20 + +
=20 + =20 + + =20 + =20 + =20 + +
Hi FakeUser,
=20 + =20 + =20 + =20 + + =20 + =20 + =20 + =20 + =20 + =20 + +
Your package is on the way but ru= +nning late. We=E2=80=99re sorry for the delay.
Now expected March 27 - March 28.= + Track your delivery for the latest updates.
=20 + =20 + =20 + =20 + =20 + + + + =20 + =20 + =20 + =20 + + + =20 + +
=20 + =20 + + =20 + =20 + =20 + +
Track your deliv= +ery
=20 + =20 + =20 + + =20 + =20 + = +=20 + =20 + +
=20 + =20 + =20 + Logitech ERGO M575 Wireless Trackball Mouse,...
=20 + =20 + + =20 + =20 + +
Order #123-1234567-1234567 =20 +
=20 + =20 + + =20 + =20 + =20 + =20 + +
  <= +/td>=20 +     = +
=20 + =20 + + =20 + =20 + +
This email was sent from an email ad= +dress that can't receive emails. Please don't reply to this email. <= +/td>=20 +
=20 + =20 + =20 + + +------=_Part_9124858_607956531.1616799814029-- diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 64f29526..201ed869 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -12,6 +12,7 @@ from custom_components.mail_and_packages.const import DOMAIN from custom_components.mail_and_packages.helpers import ( _generate_mp4, + amazon_exception, amazon_hub, amazon_search, cleanup_images, @@ -925,3 +926,17 @@ async def test_image_file_name( result = image_file_name(hass, config) assert ".gif" in result assert not result == "mail_none.gif" + + +async def test_amazon_exception(hass, mock_imap_amazon_exception): + result = amazon_exception(mock_imap_amazon_exception, [""]) + assert result["order"] == [ + "123-1234567-1234567", + "123-1234567-1234567", + "123-1234567-1234567", + "123-1234567-1234567", + "123-1234567-1234567", + "123-1234567-1234567", + "123-1234567-1234567", + ] + assert result["count"] == 7 From c3f9c5e8fdc45462af8ceb873e1bea3f3cdfb003 Mon Sep 17 00:00:00 2001 From: firstof9 Date: Sat, 27 Mar 2021 15:32:56 -0700 Subject: [PATCH 2/2] Adjust tests, handle IMAP errors --- custom_components/mail_and_packages/helpers.py | 4 ++++ tests/const.py | 2 ++ tests/test_helpers.py | 4 ++-- tests/test_init.py | 6 +++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/custom_components/mail_and_packages/helpers.py b/custom_components/mail_and_packages/helpers.py index cf3b5adf..5429f74e 100644 --- a/custom_components/mail_and_packages/helpers.py +++ b/custom_components/mail_and_packages/helpers.py @@ -1039,6 +1039,10 @@ def amazon_exception( _LOGGER.debug("Amazon email search address: %s", str(email_address)) (rv, sdata) = email_search(account, email_address, tfmt, subject) + + if rv != "OK": + continue + count += len(sdata[0].split()) _LOGGER.debug("Found %s Amazon exceptions", count) ordernums = get_tracking(sdata[0], account, pattern) diff --git a/tests/const.py b/tests/const.py index 20405baf..352678dc 100644 --- a/tests/const.py +++ b/tests/const.py @@ -51,6 +51,7 @@ "zpackages_delivered", "zpackages_transit", "amazon_delivered", + "amazon_exception", "amazon_hub", "amazon_packages", "capost_delivered", @@ -184,6 +185,7 @@ "port": 993, "resources": [ "amazon_delivered", + "amazon_exception", "amazon_hub", "amazon_packages", "capost_delivered", diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 201ed869..06f51bef 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -378,7 +378,7 @@ async def test_image_filename_oserr( assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 28 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 29 assert "Problem accessing file:" in caplog.text @@ -406,7 +406,7 @@ async def test_image_getctime_oserr( assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 28 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 29 assert "Problem accessing file:" in caplog.text diff --git a/tests/test_init.py b/tests/test_init.py index 73d06a93..7dff4be8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -26,13 +26,13 @@ async def test_unload_entry(hass, mock_update, mock_copy_overlays): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 28 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 29 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 28 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 29 assert len(hass.states.async_entity_ids(DOMAIN)) == 0 assert await hass.config_entries.async_remove(entries[0].entry_id) @@ -62,7 +62,7 @@ async def test_setup_entry( assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 28 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 29 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1