From 0f1e948403f186ac352b7aaa0367143084459e42 Mon Sep 17 00:00:00 2001 From: Chris <1105672+firstof9@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:27:39 -0700 Subject: [PATCH] feat: support Canda Post mail count (#1068) --- custom_components/mail_and_packages/const.py | 13 + .../mail_and_packages/helpers.py | 24 +- tests/conftest.py | 44 + tests/const.py | 25 + tests/test_emails/capost_mail.eml | 1735 +++++++++++++++++ tests/test_helpers.py | 28 + 6 files changed, 1866 insertions(+), 3 deletions(-) create mode 100644 tests/test_emails/capost_mail.eml diff --git a/custom_components/mail_and_packages/const.py b/custom_components/mail_and_packages/const.py index 483bbd7b..4fea8445 100644 --- a/custom_components/mail_and_packages/const.py +++ b/custom_components/mail_and_packages/const.py @@ -38,6 +38,7 @@ ATTR_EMAIL = "email" ATTR_SUBJECT = "subject" ATTR_BODY = "body" +ATTR_BODY_COUNT = "body_count" ATTR_PATTERN = "pattern" ATTR_USPS_MAIL = "usps_mail" @@ -290,6 +291,12 @@ "capost_delivering": {}, "capost_packages": {}, "capost_tracking": {}, + "capost_mail": { + "email": ["donotreply-nepasrepondre@communications.canadapost-postescanada.ca"], + "subject": ["You have mail on the way"], + "body": ["\\sYou have (\\d) pieces of mail\\s"], + "body_count": True, + }, # DHL "dhl_delivered": { "email": [ @@ -838,6 +845,12 @@ icon="mdi:truck-delivery", key="capost_delivering", ), + "capost_mail": SensorEntityDescription( + name="Mail Canada Post Mail", + native_unit_of_measurement="piece(s)", + icon="mdi:mailbox-up", + key="capost_mail", + ), "capost_packages": SensorEntityDescription( name="Mail Canada Post Packages", native_unit_of_measurement="package(s)", diff --git a/custom_components/mail_and_packages/helpers.py b/custom_components/mail_and_packages/helpers.py index 132ba0b9..9ba0640a 100644 --- a/custom_components/mail_and_packages/helpers.py +++ b/custom_components/mail_and_packages/helpers.py @@ -58,6 +58,7 @@ AMAZON_TIME_PATTERN_END, ATTR_AMAZON_IMAGE, ATTR_BODY, + ATTR_BODY_COUNT, ATTR_CODE, ATTR_COUNT, ATTR_EMAIL, @@ -1057,7 +1058,11 @@ def get_count( ) if server_response == "OK" and data[0] is not None: if ATTR_BODY in SENSOR_DATA[sensor_type].keys(): - count += find_text(data, account, SENSOR_DATA[sensor_type][ATTR_BODY]) + body_count = SENSOR_DATA[sensor_type].get(ATTR_BODY_COUNT, False) + _LOGGER.debug("Check body for mail count? %s", body_count) + count += find_text( + data, account, SENSOR_DATA[sensor_type][ATTR_BODY], body_count + ) else: count += len(data[0].split()) @@ -1151,7 +1156,9 @@ def get_tracking( return tracking -def find_text(sdata: Any, account: Type[imaplib.IMAP4_SSL], search_terms: list) -> int: +def find_text( + sdata: Any, account: Type[imaplib.IMAP4_SSL], search_terms: list, count: bool +) -> int: """Filter for specific words in email. Return count of items found as integer @@ -1175,7 +1182,18 @@ def find_text(sdata: Any, account: Type[imaplib.IMAP4_SSL], search_terms: list) email_msg = part.get_payload(decode=True) email_msg = email_msg.decode("utf-8", "ignore") pattern = re.compile(rf"{search}") - if (found := pattern.findall(email_msg)) and len(found) > 0: + if ( + count + and (found := pattern.search(email_msg)) + and len(found.groups()) > 0 + ): + _LOGGER.debug( + "Found (%s) in email result: %s", + search, + str(found.groups()), + ) + count = int(found.group(1)) + elif (found := pattern.findall(email_msg)) and len(found) > 0: _LOGGER.debug( "Found (%s) in email %s times.", search, str(len(found)) ) diff --git a/tests/conftest.py b/tests/conftest.py index ae86431a..6c256075 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ from tests.const import ( FAKE_CONFIG_DATA, FAKE_CONFIG_DATA_AMAZON_FWD_STRING, + FAKE_CONFIG_DATA_CAPOST, FAKE_CONFIG_DATA_CUSTOM_IMG, FAKE_CONFIG_DATA_EXTERNAL, FAKE_CONFIG_DATA_MISSING_TIMEOUT, @@ -164,6 +165,22 @@ async def integration_fixture_7(hass): return entry +@pytest.fixture(name="integration_capost") +async def integration_fixture_8(hass): + """Set up the mail_and_packages integration.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="imap.test.email", + data=FAKE_CONFIG_DATA_CAPOST, + version=CONFIG_VER, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + @pytest.fixture() def mock_imap(): """Mock imap class values.""" @@ -1740,3 +1757,30 @@ def mock_imap_amazon_otp(): mock_conn.fetch.return_value = ("OK", [(b"", email_file.encode("utf-8"))]) mock_conn.select.return_value = ("OK", []) yield mock_conn + + +@pytest.fixture() +def mock_imap_capost_mail(): + """Mock imap class values.""" + with patch( + "custom_components.mail_and_packages.helpers.imaplib" + ) as mock_imap_capost_mail: + mock_conn = mock.Mock(autospec=imaplib.IMAP4_SSL) + mock_imap_capost_mail.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"]) + mock_conn.uid.return_value = ("OK", [b"1"]) + f = open("tests/test_emails/capost_mail.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/const.py b/tests/const.py index ec95079f..fa8d5c34 100644 --- a/tests/const.py +++ b/tests/const.py @@ -924,3 +924,28 @@ "username": "user@fake.email", "verify_ssl": False, } + +FAKE_CONFIG_DATA_CAPOST = { + "allow_external": False, + "custom_img": False, + "folder": '"INBOX"', + "generate_mp4": False, + "gif_duration": 5, + "host": "imap.test.email", + "image_name": "mail_today.gif", + "image_path": "custom_components/mail_and_packages/images/", + "image_security": True, + "imap_security": "SSL", + "imap_timeout": 30, + "password": "suchfakemuchpassword", + "port": 993, + "resources": [ + "zpackages_delivered", + "zpackages_transit", + "capost_mail", + ], + "scan_interval": 20, + "storage": ".storage/mail_and_packages/images/", + "username": "user@fake.email", + "verify_ssl": False, +} diff --git a/tests/test_emails/capost_mail.eml b/tests/test_emails/capost_mail.eml new file mode 100644 index 00000000..c896aca2 --- /dev/null +++ b/tests/test_emails/capost_mail.eml @@ -0,0 +1,1735 @@ +Delivered-To: john.doe@gmail.com +From: Canada Post +Subject: You have mail on the way +Date: Tue, 11 Feb 2025 18:53:48 +0100 +To: john.doe@gmail.com +Reply-To: Canada Post +MIME-Version: 1.0 +Content-Type: multipart/alternative; charset="windows-1252"; boundary="----=_NextPart_989_F7AB20E0.F7AB20E0" + +------=_NextPart_989_F7AB20E0.F7AB20E0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + + You have mail on the way See what’s being delivered to your mailbox soon.‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌ ‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌ + + + + + + + + + + +Canada post + + + + + Browser view + + + + + + + + + +Represents enthusiasm about receiving new mail. + + + + + + Tuesday, February 11, 2025 + + + + You have 3 pieces of mail + + + Hi ya jabroni, + + + + Your neighbourhood letter carrier just picked up mail for you. You’ll receive the following mail: + + + + + Arriving soon: 3 items + + + + + + RADDAR + + +RADDAR + + +1 + +1 piece + + + + + + + + + CRUISE SHIP CENTERS + + +CRUISE SHIP CENTERS + + +1 + +1 piece + + + + + + + + + TIMELESS WINDOWS + + +TIMELESS WINDOWS + + +1 + +1 piece + + + + + + + + + + + + + + +Don’t forget other mail items may be on the way. Find out what mail items may not appear here. + + + + + + + + + + + + +We want to hear from you + + + +Let us know what you think of this service or report any issues. + + + + + + Send feedback + + + + +Go to Canada Post app + + + + + + + + + + +You are receiving this email as a participant of MyMail. If you received this message in error, please delete it immediately. This email contains proprietary information and is confidential. Any unauthorized distribution or copying of this message is strictly prohibited. + +Please do not reply. This email was sent from a notification-only address that cannot accept replies or other incoming emails. + +To stop receiving MyMail email notifications, opt-out through your profile in the Canada Post app. + + +Unsubscribe + +You are receiving this email because you requested to receive email notifications from Canada Post about contests, promotions, products and services. You may unsubscribe at any time. + + + + + + + + + + + + + + + +Follow us on Facebook +Follow us on Twitter +Follow us on Instagram +Follow us on LinkedIn +Follow us on YouTube + + + +canadapost.ca +2701 Riverside Drive, Ottawa ON K1A 0B1 + + + +Accessibility | Legal | Privacy + + + + + + + + + + +© Canada Post Corporation + + + + + +Government of Canada's logo + + + + + + + +------=_NextPart_989_F7AB20E0.F7AB20E0 +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + + + + + +=20 + + + + + =20 + + +You have mail on the way + + + + + + +
+=20 +=20 +=20 +See what=E2=80=99s being delivered to your mailbox soon. +=20 + + +
+ + + + +
+
+ + =20 + + + +
+ =20 + + + +
 
=20 +
 
+ + + + + + + + =20 + =20 +
+ 3D"Canada +
+ + + + + + + +
+ +

+ =20 + Browser view +

+ =20 + +
+ +
+
 
=20 +
 
= +=20 + +
+
+
+
+ 3D"Represents +
+
+
 
=20 + =20 + + + =20 + + +
+ =20 + + + + + =20 + +
+

+ Tuesday, February 11, 2025=20 +

+ + + + + + +
= + 
+

+ + You have 3 pieces of mail + +

+ + + + + + +
= + 
+

+ + Hi kyle, + +

+ + + + + + +
= + 
+

+ + + Your neighbourhood letter carrier just picked up mail for y= +ou. You=E2=80=99ll receive the following mail: + +

+ + + + + + + +
= + 
+ +
+ =20 +
=20 + =20 + + +
+ + + + =20 + =20 + + + + + + + + + =20 + +=20 + +=20 + + + + +

+ + Arriving soon: 3 items + +

+ + + + + + +
= + 
+ + + + =20 + + + + + + + + + +
+ + + + + + + + +
+ + + + 3D"RADDAR" + + +

RADDAR

+
=20 + =20 +

1

+

1 piece

+ =20 + =20 +
+
+ =20 + <= +/tr>
 
+ + + + + + + + + +
+ + + + + + + + +
+ + + + 3D"CRUISE + + +

CRUISE SHIP CENTERS

+
=20 + =20 +

1

+

1 piece

+ =20 + =20 +
+
+ =20 + <= +/tr>
 
+ + + + + + + + + +
+ + + + + + + + +
+ + + + 3D"TIMELESS + + +

TIMELESS WINDOWS

+
=20 + =20 +

1

+

1 piece

+ =20 + =20 +
+
+ =20 + <= +/tr>
 
+ +=20 + + + =20 + + =20 +
=20 + =20 + + =20 + + =20 + + +
+
+ + + + +
+ 3D""=20 + +

Don=E2=80= +=99t forget other mail items may be on the way. Find out what mail items may not appear= + here.

+
+ =20 +
+ =20 +
+ =20 +
 
+ =20 + =20 + + =20 + =20 + + +
+ =20 + + + + + + + + +
+ + 3D"" + + + + + + + +
 
+ =20 +

We want to hear from you

+ + + + + + +
= + 
+

Let us kn= +ow what you think of this service or report any issues.

+ + + + + + +
= + 
+ + + + +
+ + + + +
+ +
+ +=20 + Send feedback =20 + + +
+ +
+
+ + + + + + +
= + 
+ Go = +to Canada Post app + +
+=20 + + + =20 +
=20 + =20 + + + + + + + + +
+ + + + + + +
+ + + + + +
+ + + + + + +
 
+

You are receiving this email as a participant of MyMail. If you receiv= +ed this message in error, please delete it immediately. This email contains= + proprietary information and is confidential. Any unauthorized distribution= + or copying of this message is strictly prohibited. +
 
+ Please do not reply. This email was sent from a notification-on= +ly address that cannot accept replies or other incoming emails.=20 +
 
+ To stop receiving MyMail email notifications, opt-out through y= +our profile in the Canada Post app.

=20 +
=20 +

Unsubscribe

+

You= + are receiving this email because you requested to receive email notificati= +ons from Canada Post about contests, promotions, products and services. You may unsubscribe at any time.

+ + + + + + +
 
+
+
+ + + + + + +
&#x= +A0;
+ + + + + + +
+ + + + + +
+ + + + + + +
 
+ + + + + + + =20 + +
+ 3D"Follow     + + 3D"Follow     + 3D"Follow     + 3D"Follow     + 3D"Follow    
+ + + + + + + +
 
+

+ canadapost.ca +
2701 Riverside Drive, Ottawa ON  K1A 0B1

+ + + + + + +
 
+

+ Accessibility |  + Legal |  + Privacy

+ + + + + + +
 
+
+
+ + + + + + + +
+ + + + +
+

© Canada Post Corporation

+
+
+ + + + + + +
= + 
+ =20 +
+ +=20 + =20 + + + =20 +
+ +
+
+ 3D'' + + +------=_NextPart_989_F7AB20E0.F7AB20E0-- \ No newline at end of file diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 4df4fac2..742199e8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1182,6 +1182,7 @@ async def test_get_resourcs(hass): "buildinglink_delivered": "Mail BuildingLink Delivered", "capost_delivered": "Mail Canada Post Delivered", "capost_delivering": "Mail Canada Post Delivering", + "capost_mail": "Mail Canada Post Mail", "capost_packages": "Mail Canada Post Packages", "dhl_delivered": "Mail DHL Delivered", "dhl_delivering": "Mail DHL Delivering", @@ -1318,3 +1319,30 @@ async def test_generate_grid_image( stdout=-3, stderr=-3, ) + + +@pytest.mark.asyncio +async def test_capost_mail( + hass, + integration_capost, + mock_imap_capost_mail, + mock_osremove, + mock_osmakedir, + mock_listdir, + mock_os_path_splitext, + mock_image, + mock_resizeimage, + mock_copyfile, + caplog, +): + hass.config.internal_url = "http://127.0.0.1:8123/" + entry = integration_capost + config = entry.data.copy() + + state = hass.states.get(MAIL_IMAGE_SYSTEM_PATH) + assert state is not None + assert "/testing_config/custom_components/mail_and_packages/images/" in state.state + state = hass.states.get(MAIL_IMAGE_URL_ENTITY) + assert state.state == "unknown" + result = process_emails(hass, config) + assert result["capost_mail"] == 3