From c8b44237336890a893b3f8c530029c9f8b7fadd3 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Fri, 19 Apr 2024 09:44:41 -0400
Subject: [PATCH 01/48] DIG-4317 Implements sanitation email functionality
---
.../modules/bos_email/bos_email.http | 895 ++++++++++++++++++
.../bos_email/bos_email.links.menu.yml | 6 +
.../bos_email/bos_email.links.task.yml | 5 +
.../modules/bos_email/bos_email.module | 206 ----
.../modules/bos_email/bos_email.routing.yml | 8 +
.../modules/bos_email/http-client.env.json | 9 +
.../modules/bos_email/src/CobEmail.php | 8 +-
.../src/Controller/DrupalmailAPI.php | 4 +-
.../bos_email/src/Controller/PostmarkAPI.php | 54 +-
.../bos_email/src/Controller/PostmarkOps.php | 8 +-
.../bos_email/src/EmailTemplateInterface.php | 4 +-
.../modules/bos_email/src/Form/ConfigForm.php | 267 ++++++
.../QueueWorker/ContactformProcessItems.php | 11 +-
.../QueueWorker/ScheduledEmailProcessor.php | 60 ++
.../bos_email/src/Templates/Sanitation.php | 100 ++
15 files changed, 1420 insertions(+), 225 deletions(-)
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.menu.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.task.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Form/ConfigForm.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
new file mode 100644
index 0000000000..aee952d5e7
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -0,0 +1,895 @@
+###
+# group: bos_email / session token
+# @name Request a client-side token
+# This is **step 1** of a 2 step process to submit the contact form to Postmark for relay to a CoB email recipient.
+# This step posts an empty payload to the endpoint and receives back a token which is saved on the form and then used for submit form action (step 2).
+
+POST {{url}}/rest/email_token/create
+Authorization: Token {{bos_email_bearer_token}}
+
+> {%
+ client.test("Status code is 200", function () {
+ client.assert(response.status == 200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Response contains 'token_session'", function () {
+ client.assert(typeof response.body.token_session !== "undefined", "Response body is not 'token_session'");
+ });
+ client.global.set("email_session_token", response.body.token_session);
+%}
+
+###
+# group: bos_email / session token
+# @name Use client-side token for Contact Form
+POST {{url}}/rest/email_session/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/x-www-form-urlencoded
+
+email[token_session] = {{email_session_token}} &
+email[to_address] = {{email_test_recipient}} &
+email[name] = Boston Resident &
+email[from_address] = boston_resident_1921@gmail.com &
+email[subject] = Test Contact Form Submission &
+email[message] = Manual test contact form submission from Postman. &
+email[url] = https://www.boston.gov/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] =
+
+> {%
+ client.global.clear("email_session_token");
+ client.test("Status code is 200", function () {
+ client.assert(response.status == 200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Response status reports 'Success'", function () {
+ client.assert(response.body.status == "success", "Response body.status is not 'success'");
+ });
+ client.test("Response message is 'Message Sent'", function () {
+ client.assert(response.body.response == "Message sent", "Response message is not 'Message sent'");
+ });
+%}
+
+###
+# group: bos_email / sanitation
+# @name Sanitation on-demand: Bearer Token
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "confirmation"
+}
+
+> {%
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent" || response.body.response == "Message queued", "Response message is not expected");
+ });
+%}
+
+###
+# group: bos_email / sanitation
+# @name Sanitation scheduled: Bearer Token
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "reminder1",
+ "senddatetime": "10/02/2024 15:00"
+}
+
+> {%
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message scheduled", "Response message is not 'Message scheduled'");
+ });
+ client.test("Response contains email id", function () {
+ client.assert(typeof response.body.id !== "undefined" && response.body.id != "", "Response does not contain an ID field");
+ });
+%}
+
+###
+# group: bos_email / contact form
+# @name Contact Form: Bearer Token
+POST {{url}}/rest/email/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: multipart/form-data; boundary=WebAppBoundary
+Authorization: Token {{bos_email_bearer_token}}
+
+--WebAppBoundary
+Content-Disposition: form-data; name="email[name]"
+
+Boston Resident
+--WebAppBoundary
+Content-Disposition: form-data; name="email[from_address]"
+
+boston_resident_1921@gmail.com
+--WebAppBoundary
+Content-Disposition: form-data; name="email[url]"
+
+https://www.boston.gov/somepage
+--WebAppBoundary
+Content-Disposition: form-data; name="email[to_address]"
+
+{{email_test_recipient}}
+--WebAppBoundary
+Content-Disposition: form-data; name="email[message]"
+
+Why are the road ways in the city a boring grey?
+We should embrace our heritage and make them all green. If we can't get agreement from everyone to do this, maybe we could just do it for St Patricks day.
+Thanks
+Paddy McTavish
+--WebAppBoundary
+Content-Disposition: form-data; name="email[subject]"
+
+Question about roadways.
+--WebAppBoundary
+Content-Disposition: form-data; name="email[useHtml]"
+
+0
+--WebAppBoundary
+Content-Disposition: form-data; name="emai[contact]"
+
+
+--WebAppBoundary
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / contact form
+# @name Contact Form HTML: Bearer Token
+
+POST {{url}}/rest/email/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/x-www-form-urlencoded
+Authorization: Token {{bos_email_bearer_token}}
+
+email[useHtml] = 1 &
+email[to_address] = {{email_test_recipient}} &
+email[name] = Boston Resident &
+email[from_address] = boston_resident_1921@gmail.com &
+email[subject] = I have a great idea. &
+email[message] = Why are the road ways in the city a boring grey?
+We should embrace our heritage and make them all green. If we can't get agreement from everyone to do this, maybe we could just do it for St Patricks day.
+Thanks
+Paddy McTavish &
+email[url] = https://www.boston.gov/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] =
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / registry
+# @name Registry: Bearer Token
+POST {{url}}/rest/email/registry
+Content-Type: application/x-www-form-urlencoded
+Authorization: Token {{bos_email_bearer_token}}
+
+email[to_address] = {{email_test_recipient}} &
+email[from_address] = marriage@boston.gov &
+email[subject] = TEST Marriage Intention Application &
+email[message] = Thank you for submitting your marriage intention application. Have questions? Contact us at 0617-635-4175 or marriage@boston.gov &
+email[sender] = City of Boston Registry &
+email[template_id] = {{registry_template_id}} &
+email[name] = Boston Resident
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / metrolist
+# @name Metrolist Initiation (form-data): Bearer Token
+POST {{url}}/rest/email/MetrolistInitiationForm
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: multipart/form-data; boundary=WebAppBoundary
+Authorization: Token {{bos_email_bearer_token}}
+
+--WebAppBoundary
+Content-Disposition: form-data; name="email[to_address]"
+
+{{email_test_recipient}}
+--WebAppBoundary
+Content-Disposition: form-data; name="email[name]"
+
+David Upton
+--WebAppBoundary
+Content-Disposition: form-data; name="email[from_address]"
+
+digital-dev@boston.gov
+--WebAppBoundary
+Content-Disposition: form-data; name="email[url]"
+
+boston.gov/here
+--WebAppBoundary
+Content-Disposition: form-data; name="email[message]"
+
+{{url}}/form/metrolist-listing?token=abctesttoken
+--WebAppBoundary
+Content-Disposition: form-data; name="email[subject]"
+
+TEST Metrolist Initiation Email
+--WebAppBoundary
+Content-Disposition: form-data; name="email[useHtml]"
+
+1
+--WebAppBoundary
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / metrolist
+# @name Metrolist Initiation (json): Bearer Token
+POST {{url}}/rest/email/MetrolistInitiationForm
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "email[name]": "MetroList Listing",
+ "email[url]": "https:\/\/boston.lndo.site\/metrolist\/listing-request",
+ "email[from_address]": "noreply@boston.gov",
+ "email[sender]": "Metrolist Listing",
+ "email[to_address]": "david.upton@boston.gov",
+ "email[message]": "https:\/\/boston.lndo.site\/form\/metrolist-listing?token=ZeNCdhEQXryMmZXNzqKCrVpcXdKWsmaC4gNMF1tZzVo",
+ "email[useHtml]": "1",
+ "email[subject]": "Your requested MetroList Listing link",
+ "hidden_name": "MetroList Listing",
+ "hidden_subject": "Your requested MetroList Listing link",
+ "hidden_message": "https:\/\/boston.lndo.site\/form\/metrolist-listing?token=ZeNCdhEQXryMmZXNzqKCrVpcXdKWsmaC4gNMF1tZzVo",
+ "token": "qDfBKuYCJud_msnfiQHDC1yx0h0gqMeyyT5AzOjg0_w"
+}
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / metrolist
+# @name Metrolist Confirmation (form-data): Bearer Token
+POST {{url}}/rest/email/MetrolistListingConfirmation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: multipart/form-data; boundary=WebAppBoundary
+Authorization: Token {{bos_email_bearer_token}}
+
+--WebAppBoundary
+Content-Disposition: form-data; name="email[to_address]"
+
+{{email_test_recipient}}
+--WebAppBoundary
+Content-Disposition: form-data; name="email[name]"
+
+David Upton
+--WebAppBoundary
+Content-Disposition: form-data; name="email[from_address]"
+
+digital-dev@boston.gov
+--WebAppBoundary
+Content-Disposition: form-data; name="email[url]"
+
+boston.gov/here
+--WebAppBoundary
+Content-Disposition: form-data; name="email[message]"
+
+https://boston.lndo.site/form/metrolist-listing?token=abctesttoken
+--WebAppBoundary
+Content-Disposition: form-data; name="email[subject]"
+
+TEST Metrolist Confirmation Email
+--WebAppBoundary
+Content-Disposition: form-data; name="email[useHtml]"
+
+1
+--WebAppBoundary
+Content-Disposition: form-data; name="property_name"
+
+Davids Property
+--WebAppBoundary
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / metrolist
+# @name Metrolist Notification (form-data): Bearer Token
+POST {{url}}/rest/email/MetrolistListingNotification
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: multipart/form-data; boundary=WebAppBoundary
+Authorization: Token {{bos_email_bearer_token}}
+
+--WebAppBoundary
+Content-Disposition: form-data; name="email[name]"
+
+David Upton
+--WebAppBoundary
+Content-Disposition: form-data; name="email[url]"
+
+boston.gov/here
+--WebAppBoundary
+Content-Disposition: form-data; name="email[sender]"
+
+Metrolist Listing
+--WebAppBoundary
+Content-Disposition: form-data; name="email[to_address]"
+
+{{email_test_recipient}}
+--WebAppBoundary
+Content-Disposition: form-data; name="email[message]"
+
+https://boston.lndo.site/form/metrolist-listing?token=abctesttoken
+--WebAppBoundary
+Content-Disposition: form-data; name="email[useHtml]"
+
+1
+--WebAppBoundary
+Content-Disposition: form-data; name="email[from_address]"
+
+digital-dev@boston.gov
+--WebAppBoundary
+Content-Disposition: form-data; name="email[subject]"
+
+TEST Metrolist Notification Email
+--WebAppBoundary
+Content-Disposition: form-data; name="property_name"
+
+Davids Property
+--WebAppBoundary
+Content-Disposition: form-data; name="new"
+
+1
+--WebAppBoundary
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+
+###
+# group: bos_email / inbound
+# @name Incoming Webhook (POSTMARK)
+POST {{url}}/rest/email/postmark/Contactform/inbound
+Content-Type: application/json
+
+{
+ "FromName": "Info",
+ "MessageStream": "inbound",
+ "From": "Info@bphc.org",
+ "FromFull": {
+ "Email": "Info@bphc.org",
+ "Name": "Info",
+ "MailboxHash": ""
+ },
+ "To": "\"Boston.gov Contact Form\" ",
+ "ToFull": [
+ {
+ "Email": "ZGF2aWQudXB0b25AYm9zdG9uLmdvdg==@web-inbound.boston.gov",
+ "Name": "Boston.gov Contact Form",
+ "MailboxHash": ""
+ }
+ ],
+ "Cc": "",
+ "CcFull": [],
+ "Bcc": "",
+ "BccFull": [],
+ "OriginalRecipient": "ZGF2aWQudXB0b25AYm9zdG9uLmdvdg==@web-inbound.boston.gov",
+ "Subject": "Automatic reply: Proper disposal of pathological liquids",
+ "MessageID": "86d5de3d-1672-414b-8d58-291d7edb735f",
+ "ReplyTo": "",
+ "MailboxHash": "",
+ "Date": "Thu, 11 May 2023 20:35:35 +0000",
+ "TextBody": "Thank you for contacting the Boston Public Health Commission (BPHC). We received your email and BPHC staff will respond to your message within three business days. If your email requires a faster response, Boston residents can call:\n\n· BPHC Main Line: (617) 534-5395 (Monday through Friday, 9am to 5pm)\n· Mayor's Hotline: (617) 635-4500 (after 5pm, during the weekends)\n\nDuring the current COVID-19 public health crisis, BPHC continues to protect, preserve, and promote the health and well-being of all Boston residents, particularly the most vulnerable. If you are a Boston resident with questions about COVID-19, you can find the most current information about COVID-19, testing, vaccines and boosters, and best health practices at our website, www.boston.gov/covid19\n\nTo find information about COVID-19 testing sites in Boston, please visit our COVID-19 Testing Site Page: www.boston.gov/covid19-testing\n\nTo find information about COVID-19 vaccine and booster sites in Boston, please visit our COVID-19 Vaccine Page: www.boston.gov/covid19-vaccines\n\nIf you are seeking a replacement COVID-19 vaccine card, you will need to reach out to the vaccine administer directly for a copy of your records. CIC Health administered vaccines at the Reggie Lewis Center Vaccine Site. You may contact them directly with vaccine questions at (888) 623-3830 or by emailing vaccine-support@cic-health.com\n\nFor those living outside Boston please contact the Massachusetts Department of Public Health at (617) 624-6000 or visit their website, www.mass.gov/covid19.\n\nPlease use the following additional resources for assistance:\n\n\n1) Boston residents call Mayor's Health Line: 617-534-5050/ Toll free: 1-800-847-0710 for information about finding a primary care provider; applying for health insurance; food pantries; Boston Public Health School lunch sites; COVID symptoms; COVID19 cleaning practices; when to call your doctor v. emergency room; donating medical supplies; and other related information.\n\n2) Boston residents call \"311\" for information about parking rules and tickets; needle/litter clean up; street cleaning; getting rid of a big item; report a broken street sign; and non-emergency COVID-related issues.\n\n3) Massachusetts residents call \"211\" for information about testing sites; COVID19 symptoms; Latest state-wide orders; benefit programs (SNAP, unemployment), Call2Talk - over the phone behavioral health services/support; and other related information.\n",
+ "HtmlBody": "\n\n\n\n\nThank you for contacting the Boston Public Health Commission (BPHC). We received your email and BPHC staff will respond to your message within three business days. If your email requires a faster response, Boston residents can call:
\n
\n· BPHC Main Line: (617) 534-5395 (Monday through Friday, 9am to 5pm)
\n· Mayor's Hotline: (617) 635-4500 (after 5pm, during the weekends)
\n
\nDuring the current COVID-19 public health crisis, BPHC continues to protect, preserve, and promote the health and well-being of all Boston residents, particularly the most vulnerable. If you are a Boston resident with questions about COVID-19, you can find\n the most current information about COVID-19, testing, vaccines and boosters, and best health practices at our website, www.boston.gov/covid19
\n
\nTo find information about COVID-19 testing sites in Boston, please visit our COVID-19 Testing Site Page: www.boston.gov/covid19-testing
\n
\nTo find information about COVID-19 vaccine and booster sites in Boston, please visit our COVID-19 Vaccine Page: www.boston.gov/covid19-vaccines
\n
\nIf you are seeking a replacement COVID-19 vaccine card, you will need to reach out to the vaccine administer directly for a copy of your records. CIC Health administered vaccines at the Reggie Lewis Center Vaccine Site. You may contact them directly with vaccine\n questions at (888) 623-3830 or by emailing vaccine-support@cic-health.com
\n
\nFor those living outside Boston please contact the Massachusetts Department of Public Health at (617) 624-6000 or visit their website, www.mass.gov/covid19.
\n
\nPlease use the following additional resources for assistance:
\n
\n
\n1) Boston residents call Mayor's Health Line: 617-534-5050/ Toll free: 1-800-847-0710 for information about finding a primary care provider; applying for health insurance; food pantries; Boston Public Health School lunch sites; COVID symptoms; COVID19 cleaning\n practices; when to call your doctor v. emergency room; donating medical supplies; and other related information.
\n
\n2) Boston residents call "311" for information about parking rules and tickets; needle/litter clean up; street cleaning; getting rid of a big item; report a broken street sign; and non-emergency COVID-related issues.
\n
\n3) Massachusetts residents call "211" for information about testing sites; COVID19 symptoms; Latest state-wide orders; benefit programs (SNAP, unemployment), Call2Talk - over the phone behavioral health services/support; and other related information.\n\n\n",
+ "StrippedTextReply": "",
+ "RawEmail": "Received: by p-pm-inboundg02c-aws-useast1c.inbound.postmarkapp.com (Postfix, from userid 996)\n\tid 9B88B453CA3; Thu, 11 May 2023 20:35:38 +0000 (UTC)\nX-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on\n\tp-pm-inboundg02c-aws-useast1c\nX-Spam-Status: No\nX-Spam-Score: 1.9\nX-Spam-Tests: DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,FORGED_SPF_HELO,\n\tHTML_MESSAGE,PDS_BAD_THREAD_QP_64,RCVD_IN_DNSWL_NONE,\n\tRCVD_IN_ZEN_BLOCKED_OPENDNS,SPF_HELO_PASS,T_SCC_BODY_TEXT_LINE,\n\tURIBL_DBL_BLOCKED_OPENDNS,URIBL_ZEN_BLOCKED_OPENDNS\nReceived-SPF: pass (gcc02-bl0-obe.outbound.protection.outlook.com: Sender is authorized to use 'GCC02-BL0-obe.outbound.protection.outlook.com' in 'helo' identity (mechanism 'include:spf.protection.outlook.com' matched)) receiver=p-pm-inboundg02c-aws-useast1c; identity=helo; helo=GCC02-BL0-obe.outbound.protection.outlook.com; client-ip=52.100.153.248\nReceived: from GCC02-BL0-obe.outbound.protection.outlook.com (mail-bl0gcc02hn2248.outbound.protection.outlook.com [52.100.153.248])\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby p-pm-inboundg02c-aws-useast1c.inbound.postmarkapp.com (Postfix) with ESMTPS id 18A04453CA2\n\tfor ; Thu, 11 May 2023 20:35:38 +0000 (UTC)\nARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;\n b=jj6N9vbdzMkbk8C9u65DF2AFH42m50PODQZzrTWWtjRlzLcxXbYHC8Kp3nBHn5EG9/zxEgNRvRQyY4bbs6ClL0cJ205BwlUVi5asrB/mClpfAryWjcYx63xKL6/lleHujD1lv1AP+v8uZTJjxLun16JrQpQn/0duv9s/MORtiTHVYWBXeRWjfvIz185ady0SySOq/+OOJNTUxAkxsa3Mc7dEB6eN5HuPyHfJ+OKp1ZKkMrsIIs4gsnw51XaRkjSaMPmSOwQQJTNTzupt6JwLG9rLSOEBmq9jIJx+BcyVW9zYDzhcfsbanUN+eNizkNz+Z/CBwTT7SS7O9wc2eOBJJQ==\nARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;\n s=arcselector9901;\n h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;\n bh=KA6k2PkR+L1ueLPEaPoMLGed3YXFrrljNLt+SeQ68gI=;\n b=bTV/qXKJCSV7D8HmtUX2UcxVdAKsJWkyzN3BdwwJQ2S7PgZSlZr3K0Bz0/ELP3P0CUvjXhpjN30I5Td1dhh1QnK9zKtSg7ZDjtEdRoOVBfwutnnM0WdTGl/e1P93WNiUfHfQoLgeUGlwm7fBRPxD005vF3LeV6BgMOxpxy2Bb8PhEeXExw6Xj/jqHgPRsTOBK3QokgzsMT3skT4hed019xME3jqz0Sb9a/n1yGkpOCgijDbAqiaVTpg39z3GlD3iBzWrziT4kEsy0BjrQXclbqJmJkPZJZRuKnLykhY8Qerdq3NbbyVK4swi0euIQO1lDV+bDdwVHNvoB5PQyp2aqQ==\nARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=pass\n action=none header.from=bphc.org; dkim=pass header.d=bphc.org; arc=none\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bphc.org; s=selector2;\n h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;\n bh=KA6k2PkR+L1ueLPEaPoMLGed3YXFrrljNLt+SeQ68gI=;\n b=pY2DTo39+ER8bre34Y2S0lgE++js3gxKb8OBIHjFx2paOwkhJveEBJHJkxkWsrE6A8Uz5FH+ozHZFufhqtpNQwBLZ+mUK5l+YFg+dYlsA8MAM3ZRlbE0Om+hC+WgLYDBwekDBdMJwsJHGzCwyqiHj9qewp9IeqL1AuB586B/L7w=\nReceived: from DM8PR09MB7205.namprd09.prod.outlook.com (2603:10b6:5:2ea::17)\n by SA1PR09MB8987.namprd09.prod.outlook.com (2603:10b6:806:28d::16) with\n Microsoft SMTP Server (version=TLS1_2,\n cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6387.22; Thu, 11 May\n 2023 20:35:35 +0000\nReceived: from DM8PR09MB7205.namprd09.prod.outlook.com ([::1]) by\n DM8PR09MB7205.namprd09.prod.outlook.com ([fe80::4c61:f919:1a19:e21a%4]) with\n Microsoft SMTP Server id 15.20.6387.020; Thu, 11 May 2023 20:35:35 +0000\nFrom: Info \nTo: Boston.gov Contact Form \nSubject: Automatic reply: Proper disposal of pathological liquids\nThread-Topic: Proper disposal of pathological liquids\nThread-Index: AQHZhEgj+jgKWSu0QUGFLyYcmlnYGa9Vh7ci\nDate: Thu, 11 May 2023 20:35:35 +0000\nMessage-ID:\n <2dca75707c624d9e9fa2ca731155aa34@DM8PR09MB7205.namprd09.prod.outlook.com>\nReferences: \nIn-Reply-To: \nX-MS-Has-Attach:\nX-Auto-Response-Suppress: All\nX-MS-Exchange-Inbox-Rules-Loop: info@bphc.org\nX-MS-TNEF-Correlator:\nauthentication-results: dkim=none (message not signed)\n header.d=none;dmarc=none action=none header.from=bphc.org;\nx-ms-exchange-parent-message-id:\n \nauto-submitted: auto-generated\nx-ms-exchange-generated-message-source: Mailbox Rules Agent\nx-ms-publictraffictype: Email\nx-ms-traffictypediagnostic: DM8PR09MB7205:EE_|SA1PR09MB8987:EE_\nx-ms-office365-filtering-correlation-id: febdd29a-c5c3-4f17-c55a-08db525f4636\nx-ms-exchange-senderadcheck: 1\nx-ms-exchange-antispam-relay: 0\nx-microsoft-antispam: BCL:0;\nx-microsoft-antispam-message-info:\n xAGG2I7C6b/RY4hW+qwrkIoXgwl1ofcFIeVK2ehyn/jQXkJHANV4/yhBBe7T5zX7lYfL5eknK0zN+tzvzVBxqibPBCvxaNsj+CLFNOp097SO3RaePW4Yt88AQM1/O9OSQFOEhEB7qSN35Zqy6VC7/ijKH8aCENL4u6KNayclw/6g31jgAIrHxpiGZZA/mvLy6NNpdsQOUjR85mEvsP1+BirOALWzsmSgSU1peSo0P9vWU3rUWkjMjqtlDzr7Tr2knzTXgq5mR+gDUQ87OHlZms/i4qC5A6Y1BlW5937NGi9Lh4N3wh+I94ak63BT8CwCfIO/XVpMy2AEJaJkholv3D4DzAMFn7y8rkaZi3i3xauUMAuJuFRx5mFjGTFSdCkd8YypUotYToXFGgRpxPjwJFN7swABHgr1t2xhO4JR1LSIE9apqheptYzUZZhv8P77D9WTBozD1aohHZN2S4GY3iwHSPuVrX1pXeXU7Oe+9pb3iHBbfUCQYzn64Hi0gzfK+SMWCPHrJTL6+TCNiR6FkcfwZV0v7TKZDaqrnMUJm1x7yiCWwHHI7umVuE3MUSj9G4dd5har2NP2qhmFWVXYFn242ca0t5adA16jatn71fQ=\nx-forefront-antispam-report:\n CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:DM8PR09MB7205.namprd09.prod.outlook.com;PTR:;CAT:NONE;SFS:(13230028)(50650200015)(396003)(39830400003)(136003)(366004)(451199021)(42882007)(41300700001)(108616005)(83380400001)(9686003)(6506007)(71200400001)(24736004)(7696005)(508600001)(88996005)(66556008)(64756008)(66476007)(6916009)(66446008)(41320700001)(55016003)(66946007)(78352004)(122000001)(5660300002)(2906002)(15974865002)(40140700001)(8936002)(8676002)(111220200008)(80100003);DIR:OUT;SFP:1501;\nx-ms-exchange-antispam-messagedata-chunkcount: 1\nx-ms-exchange-antispam-messagedata-0:\n +yQivJt1LYRQ9uHQpvCyqgLrQU5JX17Wrv4326Wc8ljS1ti5FFVKUzOxvbecQfyhsr5RePRN+ZaidXzttA2AGq9LHpczP3y51IbuS0l1MInLDAM1d7uyuLsOe6XkB94BUm1UjUFwd0dxrxEcGGwbmhrp1KCIzDgqjVIxOSoeGuIi5EJpX6xL5GJfkrD0ZsF8hat8E6EOrVt/8vBrORb4TMF1qlAHtM8hODwfNF+fzZOMm85CPau9mfe0aT2mbSUYNHIYIBrho12HlcjRu9dSs2PQPGKXmfiLn/3dyZZwC1+fz/DGX6+0y13R6YPwhDJU\nContent-Type: multipart/alternative;\n\tboundary=\"_000_2dca75707c624d9e9fa2ca731155aa34DM8PR09MB7205namprd09pr_\"\nMIME-Version: 1.0\nX-OriginatorOrg: bphc.org\nX-MS-Exchange-CrossTenant-AuthAs: Internal\nX-MS-Exchange-CrossTenant-AuthSource: DM8PR09MB7205.namprd09.prod.outlook.com\nX-MS-Exchange-CrossTenant-Network-Message-Id: febdd29a-c5c3-4f17-c55a-08db525f4636\nX-MS-Exchange-CrossTenant-originalarrivaltime: 11 May 2023 20:35:35.5953\n (UTC)\nX-MS-Exchange-CrossTenant-fromentityheader: Hosted\nX-MS-Exchange-CrossTenant-id: ff5b5bc8-925b-471f-942a-eb176c03ab36\nX-MS-Exchange-Transport-CrossTenantHeadersStamped: SA1PR09MB8987\n\n--_000_2dca75707c624d9e9fa2ca731155aa34DM8PR09MB7205namprd09pr_\nContent-Type: text/plain; charset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\nThank you for contacting the Boston Public Health Commission (BPHC). We rec=\neived your email and BPHC staff will respond to your message within three b=\nusiness days. If your email requires a faster response, Boston residents ca=\nn call:\n\n=B7 BPHC Main Line: (617) 534-5395 (Monday through Friday, 9am to 5pm)\n=B7 Mayor's Hotline: (617) 635-4500 (after 5pm, during the weekends)\n\nDuring the current COVID-19 public health crisis, BPHC continues to protect=\n, preserve, and promote the health and well-being of all Boston residents, =\nparticularly the most vulnerable. If you are a Boston resident with questio=\nns about COVID-19, you can find the most current information about COVID-19=\n, testing, vaccines and boosters, and best health practices at our website,=\n www.boston.gov/covid19\n\nTo find information about COVID-19 testing sites in Boston, please visit ou=\nr COVID-19 Testing Site Page: www.boston.gov/covid19-testing\n\nTo find information about COVID-19 vaccine and booster sites in Boston, ple=\nase visit our COVID-19 Vaccine Page: www.boston.gov/covid19-vaccines\n\nIf you are seeking a replacement COVID-19 vaccine card, you will need to re=\nach out to the vaccine administer directly for a copy of your records. CIC =\nHealth administered vaccines at the Reggie Lewis Center Vaccine Site. You m=\nay contact them directly with vaccine questions at (888) 623-3830 or by ema=\niling vaccine-support@cic-health.com\n\nFor those living outside Boston please contact the Massachusetts Department=\n of Public Health at (617) 624-6000 or visit their website, www.mass.gov/co=\nvid19.\n\nPlease use the following additional resources for assistance:\n\n\n1) Boston residents call Mayor's Health Line: 617-534-5050/ Toll free: 1-80=\n0-847-0710 for information about finding a primary care provider; applying =\nfor health insurance; food pantries; Boston Public Health School lunch site=\ns; COVID symptoms; COVID19 cleaning practices; when to call your doctor v. =\nemergency room; donating medical supplies; and other related information.\n\n2) Boston residents call \"311\" for information about parking rules and tick=\nets; needle/litter clean up; street cleaning; getting rid of a big item; re=\nport a broken street sign; and non-emergency COVID-related issues.\n\n3) Massachusetts residents call \"211\" for information about testing sites; =\nCOVID19 symptoms; Latest state-wide orders; benefit programs (SNAP, unemplo=\nyment), Call2Talk - over the phone behavioral health services/support; and =\nother related information.\n\n--_000_2dca75707c624d9e9fa2ca731155aa34DM8PR09MB7205namprd09pr_\nContent-Type: text/html; charset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\n\n\n\n\n\nThank you for contacting the Boston Public Health Commission (BPHC). We rec=\neived your email and BPHC staff will respond to your message within three b=\nusiness days. If your email requires a faster response, Boston residents ca=\nn call:
\n
\n=B7 BPHC Main Line: (617) 534-5395 (Monday through Friday, 9am to 5pm)
\n=B7 Mayor's Hotline: (617) 635-4500 (after 5pm, during the weekends)
\n
\nDuring the current COVID-19 public health crisis, BPHC continues to protect=\n, preserve, and promote the health and well-being of all Boston residents, =\nparticularly the most vulnerable. If you are a Boston resident with questio=\nns about COVID-19, you can find\n the most current information about COVID-19, testing, vaccines and booster=\ns, and best health practices at our website, www.boston.gov/covid19 <=\nbr>\n
\nTo find information about COVID-19 testing sites in Boston, please visit ou=\nr COVID-19 Testing Site Page: www.boston.gov/covid19-testing
\n
\nTo find information about COVID-19 vaccine and booster sites in Boston, ple=\nase visit our COVID-19 Vaccine Page: www.boston.gov/covid19-vaccines
\n
\nIf you are seeking a replacement COVID-19 vaccine card, you will need to re=\nach out to the vaccine administer directly for a copy of your records. CIC =\nHealth administered vaccines at the Reggie Lewis Center Vaccine Site. You m=\nay contact them directly with vaccine\n questions at (888) 623-3830 or by emailing vaccine-support@cic-health.com<=\nbr>\n
\nFor those living outside Boston please contact the Massachusetts Department=\n of Public Health at (617) 624-6000 or visit their website, www.mass.gov/co=\nvid19.
\n
\nPlease use the following additional resources for assistance:
\n
\n
\n1) Boston residents call Mayor's Health Line: 617-534-5050/ Toll free: 1-80=\n0-847-0710 for information about finding a primary care provider; applying =\nfor health insurance; food pantries; Boston Public Health School lunch site=\ns; COVID symptoms; COVID19 cleaning\n practices; when to call your doctor v. emergency room; donating medical su=\npplies; and other related information.
\n
\n2) Boston residents call "311" for information about parking rule=\ns and tickets; needle/litter clean up; street cleaning; getting rid of a bi=\ng item; report a broken street sign; and non-emergency COVID-related issues=\n.
\n
\n3) Massachusetts residents call "211" for information about testi=\nng sites; COVID19 symptoms; Latest state-wide orders; benefit programs (SNA=\nP, unemployment), Call2Talk - over the phone behavioral health services/sup=\nport; and other related information.\n\n\n\n--_000_2dca75707c624d9e9fa2ca731155aa34DM8PR09MB7205namprd09pr_--\n",
+ "Tag": "",
+ "Headers": [
+ {
+ "Name": "Return-Path",
+ "Value": ""
+ },
+ {
+ "Name": "Received",
+ "Value": "by p-pm-inboundg02c-aws-useast1c.inbound.postmarkapp.com (Postfix, from userid 996)\tid 9B88B453CA3; Thu, 11 May 2023 20:35:38 +0000 (UTC)"
+ },
+ {
+ "Name": "X-Spam-Checker-Version",
+ "Value": "SpamAssassin 3.4.0 (2014-02-07) on\tp-pm-inboundg02c-aws-useast1c"
+ },
+ {
+ "Name": "X-Spam-Status",
+ "Value": "No"
+ },
+ {
+ "Name": "X-Spam-Score",
+ "Value": "1.9"
+ },
+ {
+ "Name": "X-Spam-Tests",
+ "Value": "DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,FORGED_SPF_HELO,\tHTML_MESSAGE,PDS_BAD_THREAD_QP_64,RCVD_IN_DNSWL_NONE,\tRCVD_IN_ZEN_BLOCKED_OPENDNS,SPF_HELO_PASS,T_SCC_BODY_TEXT_LINE,\tURIBL_DBL_BLOCKED_OPENDNS,URIBL_ZEN_BLOCKED_OPENDNS"
+ },
+ {
+ "Name": "Received-SPF",
+ "Value": "pass (gcc02-bl0-obe.outbound.protection.outlook.com: Sender is authorized to use 'GCC02-BL0-obe.outbound.protection.outlook.com' in 'helo' identity (mechanism 'include:spf.protection.outlook.com' matched)) receiver=p-pm-inboundg02c-aws-useast1c; identity=helo; helo=GCC02-BL0-obe.outbound.protection.outlook.com; client-ip=52.100.153.248"
+ },
+ {
+ "Name": "Received",
+ "Value": "from GCC02-BL0-obe.outbound.protection.outlook.com (mail-bl0gcc02hn2248.outbound.protection.outlook.com [52.100.153.248])\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\t(No client certificate requested)\tby p-pm-inboundg02c-aws-useast1c.inbound.postmarkapp.com (Postfix) with ESMTPS id 18A04453CA2\tfor ; Thu, 11 May 2023 20:35:38 +0000 (UTC)"
+ },
+ {
+ "Name": "ARC-Seal",
+ "Value": "i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; b=jj6N9vbdzMkbk8C9u65DF2AFH42m50PODQZzrTWWtjRlzLcxXbYHC8Kp3nBHn5EG9/zxEgNRvRQyY4bbs6ClL0cJ205BwlUVi5asrB/mClpfAryWjcYx63xKL6/lleHujD1lv1AP+v8uZTJjxLun16JrQpQn/0duv9s/MORtiTHVYWBXeRWjfvIz185ady0SySOq/+OOJNTUxAkxsa3Mc7dEB6eN5HuPyHfJ+OKp1ZKkMrsIIs4gsnw51XaRkjSaMPmSOwQQJTNTzupt6JwLG9rLSOEBmq9jIJx+BcyVW9zYDzhcfsbanUN+eNizkNz+Z/CBwTT7SS7O9wc2eOBJJQ=="
+ },
+ {
+ "Name": "ARC-Message-Signature",
+ "Value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector9901; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=KA6k2PkR+L1ueLPEaPoMLGed3YXFrrljNLt+SeQ68gI=; b=bTV/qXKJCSV7D8HmtUX2UcxVdAKsJWkyzN3BdwwJQ2S7PgZSlZr3K0Bz0/ELP3P0CUvjXhpjN30I5Td1dhh1QnK9zKtSg7ZDjtEdRoOVBfwutnnM0WdTGl/e1P93WNiUfHfQoLgeUGlwm7fBRPxD005vF3LeV6BgMOxpxy2Bb8PhEeXExw6Xj/jqHgPRsTOBK3QokgzsMT3skT4hed019xME3jqz0Sb9a/n1yGkpOCgijDbAqiaVTpg39z3GlD3iBzWrziT4kEsy0BjrQXclbqJmJkPZJZRuKnLykhY8Qerdq3NbbyVK4swi0euIQO1lDV+bDdwVHNvoB5PQyp2aqQ=="
+ },
+ {
+ "Name": "ARC-Authentication-Results",
+ "Value": "i=1; mx.microsoft.com 1; spf=none; dmarc=pass action=none header.from=bphc.org; dkim=pass header.d=bphc.org; arc=none"
+ },
+ {
+ "Name": "DKIM-Signature",
+ "Value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=bphc.org; s=selector2; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=KA6k2PkR+L1ueLPEaPoMLGed3YXFrrljNLt+SeQ68gI=; b=pY2DTo39+ER8bre34Y2S0lgE++js3gxKb8OBIHjFx2paOwkhJveEBJHJkxkWsrE6A8Uz5FH+ozHZFufhqtpNQwBLZ+mUK5l+YFg+dYlsA8MAM3ZRlbE0Om+hC+WgLYDBwekDBdMJwsJHGzCwyqiHj9qewp9IeqL1AuB586B/L7w="
+ },
+ {
+ "Name": "Received",
+ "Value": "from DM8PR09MB7205.namprd09.prod.outlook.com (2603:10b6:5:2ea::17) by SA1PR09MB8987.namprd09.prod.outlook.com (2603:10b6:806:28d::16) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6387.22; Thu, 11 May 2023 20:35:35 +0000"
+ },
+ {
+ "Name": "Received",
+ "Value": "from DM8PR09MB7205.namprd09.prod.outlook.com ([::1]) by DM8PR09MB7205.namprd09.prod.outlook.com ([fe80::4c61:f919:1a19:e21a%4]) with Microsoft SMTP Server id 15.20.6387.020; Thu, 11 May 2023 20:35:35 +0000"
+ },
+ {
+ "Name": "Thread-Topic",
+ "Value": "Proper disposal of pathological liquids"
+ },
+ {
+ "Name": "Thread-Index",
+ "Value": "AQHZhEgj+jgKWSu0QUGFLyYcmlnYGa9Vh7ci"
+ },
+ {
+ "Name": "Message-ID",
+ "Value": "<2dca75707c624d9e9fa2ca731155aa34@DM8PR09MB7205.namprd09.prod.outlook.com>"
+ },
+ {
+ "Name": "References",
+ "Value": ""
+ },
+ {
+ "Name": "In-Reply-To",
+ "Value": ""
+ },
+ {
+ "Name": "X-MS-Has-Attach",
+ "Value": ""
+ },
+ {
+ "Name": "X-Auto-Response-Suppress",
+ "Value": "All"
+ },
+ {
+ "Name": "X-MS-Exchange-Inbox-Rules-Loop",
+ "Value": "info@bphc.org"
+ },
+ {
+ "Name": "X-MS-TNEF-Correlator",
+ "Value": ""
+ },
+ {
+ "Name": "authentication-results",
+ "Value": "dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=bphc.org;"
+ },
+ {
+ "Name": "x-ms-exchange-parent-message-id",
+ "Value": ""
+ },
+ {
+ "Name": "auto-submitted",
+ "Value": "auto-generated"
+ },
+ {
+ "Name": "x-ms-exchange-generated-message-source",
+ "Value": "Mailbox Rules Agent"
+ },
+ {
+ "Name": "x-ms-publictraffictype",
+ "Value": "Email"
+ },
+ {
+ "Name": "x-ms-traffictypediagnostic",
+ "Value": "DM8PR09MB7205:EE_|SA1PR09MB8987:EE_"
+ },
+ {
+ "Name": "x-ms-office365-filtering-correlation-id",
+ "Value": "febdd29a-c5c3-4f17-c55a-08db525f4636"
+ },
+ {
+ "Name": "x-ms-exchange-senderadcheck",
+ "Value": "1"
+ },
+ {
+ "Name": "x-ms-exchange-antispam-relay",
+ "Value": "0"
+ },
+ {
+ "Name": "x-microsoft-antispam",
+ "Value": "BCL:0;"
+ },
+ {
+ "Name": "x-microsoft-antispam-message-info",
+ "Value": "xAGG2I7C6b/RY4hW+qwrkIoXgwl1ofcFIeVK2ehyn/jQXkJHANV4/yhBBe7T5zX7lYfL5eknK0zN+tzvzVBxqibPBCvxaNsj+CLFNOp097SO3RaePW4Yt88AQM1/O9OSQFOEhEB7qSN35Zqy6VC7/ijKH8aCENL4u6KNayclw/6g31jgAIrHxpiGZZA/mvLy6NNpdsQOUjR85mEvsP1+BirOALWzsmSgSU1peSo0P9vWU3rUWkjMjqtlDzr7Tr2knzTXgq5mR+gDUQ87OHlZms/i4qC5A6Y1BlW5937NGi9Lh4N3wh+I94ak63BT8CwCfIO/XVpMy2AEJaJkholv3D4DzAMFn7y8rkaZi3i3xauUMAuJuFRx5mFjGTFSdCkd8YypUotYToXFGgRpxPjwJFN7swABHgr1t2xhO4JR1LSIE9apqheptYzUZZhv8P77D9WTBozD1aohHZN2S4GY3iwHSPuVrX1pXeXU7Oe+9pb3iHBbfUCQYzn64Hi0gzfK+SMWCPHrJTL6+TCNiR6FkcfwZV0v7TKZDaqrnMUJm1x7yiCWwHHI7umVuE3MUSj9G4dd5har2NP2qhmFWVXYFn242ca0t5adA16jatn71fQ="
+ },
+ {
+ "Name": "x-forefront-antispam-report",
+ "Value": "CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:DM8PR09MB7205.namprd09.prod.outlook.com;PTR:;CAT:NONE;SFS:(13230028)(50650200015)(396003)(39830400003)(136003)(366004)(451199021)(42882007)(41300700001)(108616005)(83380400001)(9686003)(6506007)(71200400001)(24736004)(7696005)(508600001)(88996005)(66556008)(64756008)(66476007)(6916009)(66446008)(41320700001)(55016003)(66946007)(78352004)(122000001)(5660300002)(2906002)(15974865002)(40140700001)(8936002)(8676002)(111220200008)(80100003);DIR:OUT;SFP:1501;"
+ },
+ {
+ "Name": "x-ms-exchange-antispam-messagedata-chunkcount",
+ "Value": "1"
+ },
+ {
+ "Name": "x-ms-exchange-antispam-messagedata-0",
+ "Value": "+yQivJt1LYRQ9uHQpvCyqgLrQU5JX17Wrv4326Wc8ljS1ti5FFVKUzOxvbecQfyhsr5RePRN+ZaidXzttA2AGq9LHpczP3y51IbuS0l1MInLDAM1d7uyuLsOe6XkB94BUm1UjUFwd0dxrxEcGGwbmhrp1KCIzDgqjVIxOSoeGuIi5EJpX6xL5GJfkrD0ZsF8hat8E6EOrVt/8vBrORb4TMF1qlAHtM8hODwfNF+fzZOMm85CPau9mfe0aT2mbSUYNHIYIBrho12HlcjRu9dSs2PQPGKXmfiLn/3dyZZwC1+fz/DGX6+0y13R6YPwhDJU"
+ },
+ {
+ "Name": "MIME-Version",
+ "Value": "1.0"
+ },
+ {
+ "Name": "X-OriginatorOrg",
+ "Value": "bphc.org"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-AuthAs",
+ "Value": "Internal"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-AuthSource",
+ "Value": "DM8PR09MB7205.namprd09.prod.outlook.com"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-Network-Message-Id",
+ "Value": "febdd29a-c5c3-4f17-c55a-08db525f4636"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-originalarrivaltime",
+ "Value": "11 May 2023 20:35:35.5953 (UTC)"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-fromentityheader",
+ "Value": "Hosted"
+ },
+ {
+ "Name": "X-MS-Exchange-CrossTenant-id",
+ "Value": "ff5b5bc8-925b-471f-942a-eb176c03ab36"
+ },
+ {
+ "Name": "X-MS-Exchange-Transport-CrossTenantHeadersStamped",
+ "Value": "SA1PR09MB8987"
+ }
+ ],
+ "Attachments": []
+}
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / Drupal mail
+# @name Test Drupal Mail : : Bearer Token
+POST {{url}}/rest/email/test/drupal/TestDrupalmail/plain
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "To": "{{email_test_recipient}}",
+ "Cc": "",
+ "Bcc": "",
+ "Subject": "Postman: Test plaintext email",
+ "TextBody": "This is some text.\nThis is more text\\nThis is a link: https://www.mass.gov/covid19 ",
+ "Attachments": []
+}
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'Success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message Sent'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent");
+ // });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Bad Session Token
+POST {{url}}/rest/email_session/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/x-www-form-urlencoded
+
+email[token_session] = bad-session-token &
+email[to_address] = {{email_test_recipient}} &
+email[name] = Valid Email Recipient &
+email[from_address] = digital-dev@boston.gov &
+email[subject] = SHOULD NOT APPEAR IN POSTMARK &
+email[message] = This message is sent with a bad session token. &
+email[url] = https://www.boston.goc/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] =
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 401", function () {
+ // pm.response.to.have.status(403);
+ // });
+ // pm.test("Status is 'error'", function () {
+ // pm.expect(pm.response.text()).to.include("error");
+ // });
+ // pm.test("Response is 'could not authenticate'", function () {
+ // pm.expect(pm.response.text()).to.include("invalid token");
+ // });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Bad/No token in sanitation
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token asdfghjklqwertyui
+
+{
+ "to_address": {{email_test_recipient}},
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "confirmation"
+}
+
+> {%
+ client.test("Status code is 401", function () {
+ client.assert(response.status == 401, "Response HTTP Code is not 401");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response - could not authenticate", function () {
+ client.assert(response.body.response == "could not authenticate", "Response status field is not 'could not authenticate'");
+ });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Missing Bearer Token
+POST {{url}}/rest/email/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/x-www-form-urlencoded
+
+email[to_address] = {{email_test_recipient}} &
+email[name] = Valid Email Recipient &
+email[from_address] = digital-dev@boston.gov &
+email[subject] = SHOULD NOT APPEAR IN POSTMARK &
+email[message] = This message is sent with a bad token. &
+email[url] = https://www.boston.gov/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] =
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 401", function () {
+ // pm.response.to.have.status(401);
+ // });
+ // pm.test("Status is 'error'", function () {
+ // pm.expect(pm.response.text()).to.include("error");
+ // });
+ // pm.test("Response is 'could not authenticate'", function () {
+ // pm.expect(pm.response.text()).to.include("could not authenticate");
+ // });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Blocked User
+POST {{url}}/rest/email/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+#X-PM-Bounce-Type: hardbounce
+Content-Type: application/x-www-form-urlencoded
+Authorization: Token {{bos_email_bearer_token}}
+
+email[to_address] = blocked@boston.gov &
+email[name] = Blocked Email Recipient &
+email[from_address] = HardBounce@bounce-testing.postmarkapp.com &
+email[subject] = SHOULD NOT APPEAR IN POSTMARK &
+email[message] = This message is sent to a blocked user. &
+email[url] = https://www.boston.gov/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] =
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'success'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message queued'", function () {
+ // pm.expect(pm.response.text()).to.include("Message queued");
+ // });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Honeypot Fail
+POST {{url}}/rest/email/contactform
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/x-www-form-urlencoded
+Authorization: Token {{bos_email_bearer_token}}
+
+email[to_address] = {{email_test_recipient}} &
+email[name] = Test Person &
+email[from_address] = digital-dev@boston.gov &
+email[subject] = SHOULD NOT APPEAR IN POSTMARK &
+email[message] = This email has a value in the honeypot &
+email[url] = https://www.boston.gov/somepage &
+email[browser] = PostmanRuntime/7.29.2 &
+email[contact] = should be empty
+
+> {%
+ // TODO: migrate to HTTP Client Response handler API
+ // pm.test("Status code is 200", function () {
+ // pm.response.to.have.status(200);
+ // });
+ // pm.test("Status is 'error'", function () {
+ // pm.expect(pm.response.text()).to.include("success");
+ // });
+ // pm.test("Response is 'Message sent!'", function () {
+ // pm.expect(pm.response.text()).to.include("Message sent!");
+ // });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Bad email
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "to_address": "bademail.com",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "confirmation"
+}
+
+> {%
+ client.test("Status code is 400", function () {
+ client.assert(response.status == 400, "Response HTTP Code is not 400");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "error", "Response status field is not 'error'");
+ client.assert(response.body.response.indexOf("email is not valid") != false, "Response does not report a bad email address");
+ });
+%}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.menu.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.menu.yml
new file mode 100644
index 0000000000..df73761c20
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.menu.yml
@@ -0,0 +1,6 @@
+bos_email.admin.services:
+ title: 'Email Services'
+ description: 'Manage Email Services via boston.gov'
+ parent: bos_core.admin
+ route_name: bos_email.admin.services
+ weight: 1
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.task.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.task.yml
new file mode 100644
index 0000000000..bd01f92e98
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.links.task.yml
@@ -0,0 +1,5 @@
+bos_email.admin.services:
+ title: 'Email Services'
+ route_name: bos_email.admin.services
+ base_route: bos_core.admin
+ weight: 1
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
index 75b3e8a6e9..a43d15a024 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
@@ -5,212 +5,6 @@
* The Base module file for bos_email module.
*/
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_form_alter().
- */
-function bos_email_form_bos_core_admin_settings_alter(&$form, FormStateInterface $form_state) {
-
- if (!empty($form_state->getUserInput())) {
- if ($input = $form_state->getUserInput()["bos_email"]) {
- \Drupal::configFactory()->getEditable('bos_email.settings')
- ->set("enabled", $input["enabled"])
- ->set("q_enabled", $input["q_enabled"])
- ->set("contactform.enabled", $input["contactform"]["enabled"] ?? 0)
- ->set("contactform.q_enabled", $input["contactform"]["q_enabled"] ?? 0)
- ->set("registry.enabled", $input["registry"]["enabled"] ?? 0)
- ->set("registry.q_enabled", $input["registry"]["q_enabled"] ?? 0)
- ->set("commissions.enabled", $input["commissions"]["enabled"] ?? 0)
- ->set("commissions.q_enabled", $input["commissions"]["q_enabled"] ?? 0)
- ->set("metrolist.enabled", $input["metrolist"]["enabled"] ?? 0)
- ->set("metrolist.q_enabled", $input["metrolist"]["q_enabled"] ?? 0)
- ->set("alerts.recipient", $input["alerts"]["conditions"]["recipient"] ?? "")
- ->set("hardbounce.hardbounce", $input["alerts"]["hb"]["hardbounce"] ?? 0)
- ->set("hardbounce.recipient", $input["alerts"]["hb"]["recipient"] ?? "")
- ->set("alerts.recipient", $input["alerts"]["conditions"]["recipient"] ?? 0)
- ->set("alerts.token", $input["alerts"]["conditions"]["token"] ?? 0)
- ->set("alerts.honeypot", $input["alerts"]["conditions"]["honeypot"] ?? 0)
- ->set("monitor.recipient", $input["alerts"]["monitoring"]["recipient"] ?? 0)
- ->set("monitor.all", $input["alerts"]["monitoring"]["all"] ?? 0)
- ->save();
- }
- }
-
- else {
- $config = \Drupal::configFactory()->get("bos_email.settings");
- $form["bos_email"] = [
- '#type' => 'details',
- '#title' => 'PostMark Emailer',
- '#markup' => 'Fine-grain management for emails sent via PostMark.',
- '#open' => FALSE,
-
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Postmark Email Service Enabled'),
- '#description' => t('When selected, emails will be sent via Postmark. When unselected all emails are added to the queue.'),
- '#default_value' => $config->get('enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Postmark-fail Queue Enabled'),
- '#description' => t('When selected, emails that Postmark cannot process will be queued and there will be attempts to be resend. When unselected failed emails are discarded.'),
- '#default_value' => $config->get('q_enabled'),
- ],
-
- "alerts" => [
- '#type' => 'details',
- '#title' => 'PostMark Email Alerts',
- '#description' => 'Configure outbound emails for issues which arise with PostMark integration.',
- '#open' => FALSE,
-
- "conditions" => [
- '#type' => 'fieldset',
- '#title' => 'Service Abuse',
- '#markup' => 'Emails will be sent to the recipient below when these potential abuse events occur:',
- '#collapsible' => FALSE,
-
- "token" => [
- '#type' => 'checkbox',
- '#title' => t('An incorrect API authentication token is provided. This could indicate a hacking attempt or attempted spam/relay abuse.'),
- '#default_value' => $config->get('alerts.token') ?? 0,
- ],
- "honeypot" => [
- '#type' => 'checkbox',
- '#title' => t('The honeypot field (a hidden input field a \'person\' cannot see or update) in a submitted form has data in it. This could indictate hacking attempt or attempted spam/relay abuse.'),
- '#default_value' => $config->get('alerts.honeypot') ?? 0,
- ],
- "recipient" => [
- '#type' => 'textfield',
- "#title" => "Email recipient",
- "#description" => "The email (or email group) to receive hardbounce alerts.",
- "#attributes" => ["placeholder" => "someone@boston.gov"],
- "#default_value" => $config->get('alerts.recipient') ?? "",
- ],
- ],
-
- "monitoring" => [
- '#type' => 'fieldset',
- '#title' => 'Service Monitoring',
- '#markup' => 'Emails will be sent to the recipient below when these unexpected service error events occur:',
- '#collapsible' => FALSE,
- "all" => [
- '#type' => 'checkbox',
- '#title' => t('All non-abuse failures when connecting to Postmark API.'),
- '#default_value' => $config->get('monitor.all') ?? 0,
- ],
- "recipient" => [
- '#type' => 'textfield',
- "#title" => "Email recipient",
- "#description" => "The email (or email group) to receive service error emails.",
- "#attributes" => ["placeholder" => "someone@boston.gov"],
- "#default_value" => $config->get('monitor.recipient') ?? "",
- ],
- ],
-
- "hb" => [
- '#type' => 'fieldset',
- '#title' => 'Hard Bounce / Recipient Supression',
- '#markup' => 'Emails will be sent to the recipient below when the following normal conditions occur:',
- '#collapsible' => FALSE,
-
- "hardbounce" => [
- '#type' => 'checkbox',
- '#title' => t('The intended recipient is suppressed by PostMark.'),
- '#default_value' => $config->get('hardbounce.hardbounce') ?? 0,
- ],
- "recipient" => [
- '#type' => 'textfield',
- "#title" => "Email recipient",
- "#description" => "The email (or email group) to receive hardbounce alerts.",
- "#attributes" => ["placeholder" => "someone@boston.gov"],
- "#default_value" => $config->get('hardbounce.recipient') ?? "",
- ],
- ],
-
- "footnote" => ['#markup' => "NOTE: These email alerts are sent via Drupal mail."],
- ],
-
- "contactform" => [
- '#type' => 'fieldset',
- '#title' => 'Contact Form',
- '#markup' => 'Emails from the main Contact Form - when clicking on email addresses on boston.gov.',
- '#collapsible' => FALSE,
-
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Contact Form email service enabled'),
- '#default_value' => $config->get('contactform.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Contact Form queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send and are queued will be processed on each cron run.'),
- '#default_value' => $config->get('contactform.q_enabled'),
- ],
- ],
-
- "registry" => [
- '#type' => 'fieldset',
- '#title' => 'Registry Suite',
- '#markup' => 'Emails from the Registry App - confirmations.',
- '#collapsible' => FALSE,
-
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Registry email service enabled'),
- '#default_value' => $config->get('registry.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Registry queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send and are queued will be processed on each cron run.'),
- '#default_value' => $config->get('registry.q_enabled'),
- ],
- ],
-
- "commissions" => [
- '#type' => 'fieldset',
- '#title' => 'Commissions App',
- '#markup' => 'Emails from the Commissions App.',
- '#collapsible' => FALSE,
-
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Commission email service enabled'),
- '#default_value' => $config->get('commissions.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Commissions queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send and are queued will be processed on each cron run.'),
- '#default_value' => $config->get('commissions.q_enabled'),
- ],
- ],
-
- "metrolist" => [
- '#type' => 'fieldset',
- '#title' => 'Metrolist Listing Form',
- '#markup' => 'Emails sent from Metrolist Listing Form processes.',
- '#collapsible' => FALSE,
-
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('metrolist email service Enabled'),
- '#default_value' => $config->get('metrolist.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('metrolist queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send and are queued will be processed on each cron run.'),
- '#default_value' => $config->get('metrolist.q_enabled'),
- ],
- ],
-
- ];
- }
-}
-
/**
* Implements hook_mail().
*/
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
index 4d4e3c7557..d6f9325cc7 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
@@ -78,3 +78,11 @@ bos_email.drupalmail.test:
no_cache: 'TRUE'
requirements:
_access: 'TRUE'
+
+bos_email.admin.services:
+ path: '/admin/config/system/boston/email_services'
+ defaults:
+ _title: 'Email Services'
+ _form: '\Drupal\bos_email\Form\ConfigForm'
+ requirements:
+ _role: "site_administrator"
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
new file mode 100644
index 0000000000..3e03ceb99b
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
@@ -0,0 +1,9 @@
+{
+ "LOCAL for testing": {
+ "url": "https://boston.lndo.site",
+ "bos_email_bearer_token": "c43517a240ee61898c00600eaa775aa0d0e639322c3f275b780f66062f69",
+ "contact_form_session_token": "",
+ "registry_template_id": "31135208",
+ "email_test_recipient": "david.upton@boston.gov"
+ }
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
index 5d0780e7a3..ccc49caa86 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
@@ -26,6 +26,7 @@ class CobEmail {
],
"endpoint" => "",
"server" => "",
+ "senddatetime" => "",
];
private array $fieldTypes = [
@@ -48,6 +49,7 @@ class CobEmail {
"ReplyTo" => "email",
],
"endpoint" => "string",
+ "senddatetime" => "string",
];
private array $requiredFields = [
@@ -149,7 +151,7 @@ public function validate(array $data = []) {
$emailparts = explode("<", trim($email));
$mail = trim(array_pop($emailparts), " >");
if (!\Drupal::service('email.validator')->isValid($mail)) {
- $this->validation_errors[] = "{$field} email is not valid ({$value}";
+ $this->validation_errors[] = "{$field} email is not valid ({$value})";
$validated = FALSE;
}
}
@@ -549,4 +551,8 @@ public function removeEmpty() {
}
}
+ public function is_scheduled() {
+ return !empty($this->emailFields["senddatetime"]);
+ }
+
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
index ffb07eb501..0951ec010f 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
@@ -99,8 +99,8 @@ public function addQueueItem(array $data) {
* @return bool
*/
private function authenticate() {
- $postmark_auth = new PostmarkOps();
- return $postmark_auth->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
+ $email_ops = new PostmarkOps();
+ return $email_ops->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
}
/**
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php
index 51a78b1652..77f326968f 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php
@@ -12,6 +12,18 @@
/**
* Postmark class for API.
+ *
+ * This token is best used when a client-side javascript code is used to call
+ * the bos_email APIs.
+ * The workflow would be to call /rest/token/create from the server-side, save thes
+ * token which is returned and embed on the clientside. The client-side js then
+ * passes the token to the json submitted to /rest/email_token/{op} where it is
+ * validated and deleted. This way if the token is scraped from the client
+ * side, it can only be used once.
+ *
+ * For server-side sending of emails, just use the /rest/email/{op} which uses
+ * a bearer token.
+ *
*/
class PostmarkAPI extends ControllerBase {
@@ -107,14 +119,29 @@ public function addQueueItem(array $data) {
return $queue_item_id;
}
+ /**
+ * Load an email into the queue for later dispatch.
+ *
+ * @param array $data
+ * The array containing the email POST data.
+ */
+ public function addSheduledItem(array $data) {
+ $queue_name = 'scheduled_email';
+ $queue = \Drupal::queue($queue_name);
+ $queue_item_id = $queue->createItem($data);
+
+ return $queue_item_id;
+ }
+
+
/**
* Check the authentication key sent in the header is valid.
*
* @return bool
*/
private function authenticate() {
- $postmark_auth = new PostmarkOps();
- return $postmark_auth->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
+ $email_ops = new PostmarkOps();
+ return $email_ops->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
}
/**
@@ -233,12 +260,12 @@ private function sendEmail(CobEmail $email) {
}
// Send the email.
- $postmark_ops = new PostmarkOps();
- $sent = $postmark_ops->sendEmail($mailobj);
+ $email_ops = new PostmarkOps();
+ $sent = $email_ops->sendEmail($mailobj);
if (!$sent) {
// Add email data to queue because of Postmark failure.
- $mailobj["postmark_error"] = $postmark_ops->error;
+ $mailobj["postmark_error"] = $email_ops->error;
$this->addQueueItem($mailobj);
if ($this->debug) {
@@ -304,7 +331,7 @@ public function begin(string $service = 'contactform') {
->getHttpHost(), "lndo.site");
$response_array = [];
- if (in_array($service, ["contactform", "registry"])) {
+ if (in_array($service, ["contactform", "registry", "sanitation"])) {
// This is done for legacy reasons (endpoint already in production and
// in lowercase)
$service = ucwords($service);
@@ -364,7 +391,20 @@ public function begin(string $service = 'contactform') {
// Format and validate the message body.
if ($this->formatEmail($payload)) {
// Send email.
- $response_array = $this->sendEmail($payload["postmark_data"]);
+ if ($payload["postmark_data"]->is_scheduled()) {
+ $item = $payload["postmark_data"]->data();
+ $id = $this->addSheduledItem($item);
+ if ($id && is_numeric($id)) {
+ $response_array = [
+ 'status' => 'success',
+ 'response' => 'Message scheduled',
+ 'id' => $id
+ ];
+ }
+ }
+ else {
+ $response_array = $this->sendEmail($payload["postmark_data"]);
+ }
}
else {
PostmarkOps::alertHandler($payload, [], "", [], $this->error);
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
index deacdcf254..d9445561df 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
@@ -2,15 +2,17 @@
namespace Drupal\bos_email\Controller;
+use Drupal\bos_core\Controllers\Curl\BosCurlControllerBase;
use Drupal\Core\Site\Settings;
use Exception;
/**
* Postmark variables for email API.
*/
-class PostmarkOps {
+class PostmarkOps extends BosCurlControllerBase {
- public string $error;
+ // Make this protected var from BosCurlControllerBase public
+ public null|string $error;
/**
* Check token and authenticate.
@@ -198,7 +200,7 @@ public static function alertHandler($item, $response, $http_code, $config, $erro
\Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
}
}
- }
+ }
// If no other issues, but the email failed to send.
if (!isset($mailManager)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateInterface.php
index 4f4ac448ed..2d8f813652 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateInterface.php
@@ -45,9 +45,11 @@ public static function templatePlainText(array &$emailFields): void;
public static function templateHtmlText(array &$emailFields): void;
/**
+ * Returns the payload field which is a honeypot for the form submitted.
+ * NOTE: Should return "" if there is no honeypot.
+ *
* @return string The name of the honeypot field on the form (if any).
*
- * NOTE: Should return "" if there is no honeypot.
*/
public static function getHoneypotField(): string;
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Form/ConfigForm.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Form/ConfigForm.php
new file mode 100644
index 0000000000..6b27dc4fa8
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Form/ConfigForm.php
@@ -0,0 +1,267 @@
+configFactory->get(self::getEditableConfigNames()[0]);
+ $form["bos_email"] = [
+ '#type' => 'fieldset',
+ '#title' => 'City of Boston Emailer',
+ '#markup' => 'Fine-grain management for emails sent via City of Boston REST API.',
+ "#tree" => TRUE,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Current Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => [
+ "drupal" => "Drupal",
+ "postmark" => "Postmark"
+ ],
+ '#default_value' => $config->get('service')
+ ],
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Email Service Enabled'),
+ '#description' => t('When selected, emails will be sent via the indicated email service. When unselected all emails are added to the queue.'),
+ '#default_value' => $config->get('enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Email-fail Queue Enabled'),
+ '#description' => t('When selected, emails that the email service cannot process will be queued and there will be attempts to be resend. When unselected failed emails are discarded.'),
+ '#default_value' => $config->get('q_enabled'),
+ ],
+
+ "alerts" => [
+ '#type' => 'details',
+ '#title' => 'Email Service monitoring',
+ '#description' => 'Configure internal alert emails for issues which arise during operations.',
+ '#open' => FALSE,
+
+ "conditions" => [
+ '#type' => 'fieldset',
+ '#title' => 'Service Abuse',
+ '#markup' => 'Emails will be sent to the recipient below when these potential abuse events occur:',
+ '#collapsible' => FALSE,
+
+ "token" => [
+ '#type' => 'checkbox',
+ '#title' => t('An incorrect API authentication token is provided. This could indicate a hacking attempt or attempted spam/relay abuse.'),
+ '#default_value' => $config->get('alerts.token') ?? 0,
+ ],
+ "honeypot" => [
+ '#type' => 'checkbox',
+ '#title' => t('The honeypot field (a hidden input field a \'person\' cannot see or update) in a submitted form has data in it. This could indictate hacking attempt or attempted spam/relay abuse.'),
+ '#default_value' => $config->get('alerts.honeypot') ?? 0,
+ ],
+ "recipient" => [
+ '#type' => 'textfield',
+ "#title" => "Email recipient",
+ "#description" => "The email (or email group) to receive hardbounce alerts.",
+ "#attributes" => ["placeholder" => "someone@boston.gov"],
+ "#default_value" => $config->get('alerts.recipient') ?? "",
+ ],
+ ],
+
+ "monitoring" => [
+ '#type' => 'fieldset',
+ '#title' => 'Service Monitoring',
+ '#markup' => 'Emails will be sent to the recipient below when these unexpected service error events occur:',
+ '#collapsible' => FALSE,
+ "all" => [
+ '#type' => 'checkbox',
+ '#title' => t('All non-abuse failures.'),
+ '#default_value' => $config->get('monitor.all') ?? 0,
+ ],
+ "recipient" => [
+ '#type' => 'textfield',
+ "#title" => "Email recipient",
+ "#description" => "The email (or email group) to receive service error emails.",
+ "#attributes" => ["placeholder" => "someone@boston.gov"],
+ "#default_value" => $config->get('monitor.recipient') ?? "",
+ ],
+ ],
+
+ "hb" => [
+ '#type' => 'fieldset',
+ '#title' => 'Hard Bounce / Recipient Supression',
+ '#markup' => 'Emails will be sent to the recipient below when the following normal conditions occur:',
+ '#collapsible' => FALSE,
+
+ "hardbounce" => [
+ '#type' => 'checkbox',
+ '#title' => t('The intended recipient is suppressed by Email Service.'),
+ '#default_value' => $config->get('hardbounce.hardbounce') ?? 0,
+ ],
+ "recipient" => [
+ '#type' => 'textfield',
+ "#title" => "Email recipient",
+ "#description" => "The email (or email group) to receive hardbounce alerts.",
+ "#attributes" => ["placeholder" => "someone@boston.gov"],
+ "#default_value" => $config->get('hardbounce.recipient') ?? "",
+ ],
+ ],
+
+ "footnote" => ['#markup' => "NOTE: These email alerts are sent via Drupal mail."],
+ ],
+
+ "contactform" => [
+ '#type' => 'fieldset',
+ '#title' => 'Contact Form',
+ '#markup' => 'Emails from the main Contact Form - when clicking on email addresses on boston.gov.',
+ '#collapsible' => FALSE,
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Contact Form email service enabled'),
+ '#default_value' => $config->get('contactform.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Contact Form queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('contactform.q_enabled'),
+ ],
+ ],
+
+ "registry" => [
+ '#type' => 'fieldset',
+ '#title' => 'Registry Suite',
+ '#markup' => 'Emails from the Registry App - confirmations.',
+ '#collapsible' => FALSE,
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Registry email service enabled'),
+ '#default_value' => $config->get('registry.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Registry queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('registry.q_enabled'),
+ ],
+ ],
+
+ "commissions" => [
+ '#type' => 'fieldset',
+ '#title' => 'Commissions App',
+ '#markup' => 'Emails from the Commissions App.',
+ '#collapsible' => FALSE,
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Commission email service enabled'),
+ '#default_value' => $config->get('commissions.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Commissions queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('commissions.q_enabled'),
+ ],
+ ],
+
+ "metrolist" => [
+ '#type' => 'fieldset',
+ '#title' => 'Metrolist Listing Form',
+ '#markup' => 'Emails sent from Metrolist Listing Form processes.',
+ '#collapsible' => FALSE,
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Metrolist email service enabled'),
+ '#default_value' => $config->get('metrolist.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Metrolist queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('metrolist.q_enabled'),
+ ],
+ ],
+
+ "sanitation" => [
+ '#type' => 'fieldset',
+ '#title' => 'Sanitation Email Services',
+ '#markup' => 'Emails sent from Sanitation WebApp.',
+ '#collapsible' => FALSE,
+
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation email service enabled'),
+ '#default_value' => $config->get('sanitation.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('sanitation.q_enabled'),
+ ],
+ "sched_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation scheduled email processing enabled'),
+ '#description' => t('When selected, scheduled emails are queued will be processed on each cron run.'),
+ '#default_value' => $config->get('sanitation.sched_enabled'),
+ ],
+ ],
+
+ ];
+ return parent::buildForm($form, $form_state);
+ }
+
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+
+ if ($input = $form_state->getUserInput()["bos_email"]) {
+ $this->configFactory->getEditable(self::getEditableConfigNames()[0])
+ ->set("service", $input["service"])
+ ->set("enabled", $input["enabled"])
+ ->set("q_enabled", $input["q_enabled"])
+ ->set("contactform.enabled", $input["contactform"]["enabled"] ?? 0)
+ ->set("contactform.q_enabled", $input["contactform"]["q_enabled"] ?? 0)
+ ->set("registry.enabled", $input["registry"]["enabled"] ?? 0)
+ ->set("registry.q_enabled", $input["registry"]["q_enabled"] ?? 0)
+ ->set("commissions.enabled", $input["commissions"]["enabled"] ?? 0)
+ ->set("commissions.q_enabled", $input["commissions"]["q_enabled"] ?? 0)
+ ->set("metrolist.enabled", $input["metrolist"]["enabled"] ?? 0)
+ ->set("metrolist.q_enabled", $input["metrolist"]["q_enabled"] ?? 0)
+ ->set("sanitation.enabled", $input["sanitation"]["enabled"] ?? 0)
+ ->set("sanitation.sched_enabled", $input["sanitation"]["sched_enabled"] ?? 0)
+ ->set("sanitation.q_enabled", $input["sanitation"]["q_enabled"] ?? 0)
+ ->set("alerts.recipient", $input["alerts"]["conditions"]["recipient"] ?? "")
+ ->set("hardbounce.hardbounce", $input["alerts"]["hb"]["hardbounce"] ?? 0)
+ ->set("hardbounce.recipient", $input["alerts"]["hb"]["recipient"] ?? "")
+ ->set("alerts.recipient", $input["alerts"]["conditions"]["recipient"] ?? 0)
+ ->set("alerts.token", $input["alerts"]["conditions"]["token"] ?? 0)
+ ->set("alerts.honeypot", $input["alerts"]["conditions"]["honeypot"] ?? 0)
+ ->set("monitor.recipient", $input["alerts"]["monitoring"]["recipient"] ?? 0)
+ ->set("monitor.all", $input["alerts"]["monitoring"]["all"] ?? 0)
+ ->save();
+ }
+
+// parent::submitForm($form, $form_state);
+
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
index ff41ae73c4..ab0c6e9e57 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
@@ -2,6 +2,7 @@
namespace Drupal\bos_email\Plugin\QueueWorker;
+use Drupal\Core\Annotation\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\bos_email\Controller\PostmarkOps;
@@ -28,22 +29,22 @@ public function processItem($item) {
$config = \Drupal::configFactory()->get("bos_email.settings");
if (!$config->get("q_enabled")) {
- throw new \Exception("All queues are paused by settings at /admin/config/system/boston.");
+ throw new \Exception("All queues are paused by settings at /admin/config/system/boston/email_services.");
}
elseif (!empty($item["server"])
&& !$config->get(strtolower($item["server"]))["q_enabled"]) {
- throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston.");
+ throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
}
if (!empty($item["postmark_error"])) {
unset($item["postmark_error"]);
}
- $postmark_ops = new PostmarkOps();
- $postmark_send = $postmark_ops->sendEmail($item);
+ $email_ops = new PostmarkOps();
+ $postmark_send = $email_ops->sendEmail($item);
if (!$postmark_send) {
- throw new \Exception("There was a problem in bos_email:PostmarkOps. {$postmark_ops->error}");
+ throw new \Exception("There was a problem in bos_email:PostmarkOps. {$email_ops->error}");
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
new file mode 100644
index 0000000000..74a9cedcbb
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
@@ -0,0 +1,60 @@
+get("bos_email.settings");
+
+ if (!$config->get("q_enabled")) {
+ throw new \Exception("All queues are paused by settings at /admin/config/system/boston/email_services.");
+ }
+ elseif (!empty($item["server"])
+ && !$config->get(strtolower($item["server"]))["q_enabled"]) {
+ throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
+ }
+
+ if (!empty($item["postmark_error"])) {
+ unset($item["postmark_error"]);
+ }
+
+ if ($item["senddatetime"] <= strtotime("Now")) {
+ $email_ops = new PostmarkOps();
+ $postmark_send = $email_ops->sendEmail($item);
+ }
+
+ if (!$postmark_send) {
+ throw new \Exception("There was a problem in bos_email:PostmarkOps. {$email_ops->error}");
+ }
+
+ }
+ catch (\Exception $e) {
+ \Drupal::logger("contactform")->error($e->getMessage());
+ throw new \Exception($e->getMessage());
+ }
+
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
new file mode 100644
index 0000000000..28eff71628
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
@@ -0,0 +1,100 @@
+setField("endpoint", PostmarkAPI::POSTMARK_TEMPLATE_ENDPOINT);
+
+ // Set up the Postmark template.
+ $template_map = [
+ "confirmation" => "sani_confirm",
+ "reminder1" => "sani_remind1",
+ "reminder2" => "sani_remind2",
+ "cancel" => "sani_cancel",
+ ];
+ $cobdata->setField("TemplateID", $template_map[$emailFields["type"]]);
+ $cobdata->setField("TemplateModel", [
+ "subject" => $emailFields["subject"],
+ "TextBody" => $emailFields["message"],
+ "ReplyTo" => $emailFields["from_address"]
+ ]);
+ $cobdata->delField("HtmlBody");
+ $cobdata->delField("TextBody");
+ $cobdata->delField("Subject");
+
+ // Set general email fields.
+ $cobdata->setField("To", $emailFields["to_address"]);
+ $cobdata->setField("From", $emailFields["from_address"]);
+ $cobdata->setField("ReplyTo", $emailFields["from_address"]);
+
+ $cobdata->setField("Tag", $emailFields["type"]);
+
+ // is this to be scheduled?
+ if (!empty($emailFields["senddatetime"])) {
+ try {
+ $senddatetime = strtotime($emailFields["senddatetime"]);
+ $cobdata->setField("senddatetime", $senddatetime);
+ }
+ catch (Exception $e) {
+ $cobdata->delField("senddatetime");
+ }
+ }
+ else {
+ $cobdata->delField("senddatetime");
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templatePlainText(&$emailFields): void {
+ // Only use templates ATM.
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templateHtmlText(&$emailFields): void {
+ // Only use templates ATM.
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getHoneypotField(): string {
+ return "";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getServerID(): string {
+ return "sanitation";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function formatInboundEmail(array &$emailFields): void {
+ // Not Used
+ }
+
+}
From 937de38cdb7c2967df8f3fa3074260537d685dbb Mon Sep 17 00:00:00 2001
From: David Upton
Date: Mon, 22 Apr 2024 13:57:48 -0400
Subject: [PATCH 02/48] DIG-4317 Implements scheduled sending and refactors.
---
.../modules/bos_email/bos_email.http | 61 +-
.../modules/bos_email/bos_email.routing.yml | 22 +-
.../modules/bos_email/src/CobEmail.php | 80 ++-
.../src/Controller/DrupalmailAPI.php | 251 -------
.../{PostmarkAPI.php => EmailController.php} | 637 ++++++++++++------
.../bos_email/src/Controller/PostmarkOps.php | 228 -------
.../bos_email/src/Controller/TokenOps.php | 60 --
.../bos_email/src/EmailServiceInterface.php | 22 +
.../bos_email/src/EmailTemplateInterface.php | 22 +-
.../modules/bos_email/src/Form/ConfigForm.php | 72 +-
.../QueueWorker/ContactformProcessItems.php | 24 +-
.../QueueWorker/ScheduledEmailProcessor.php | 51 +-
.../bos_email/src/Services/DrupalService.php | 91 +++
.../src/Services/PostmarkService.php | 105 +++
.../bos_email/src/Services/TokenOps.php | 98 +++
.../bos_email/src/Templates/Contactform.php | 31 +-
.../src/Templates/MetrolistInitiationForm.php | 23 +-
.../MetrolistListingConfirmation.php | 23 +-
.../MetrolistListingNotification.php | 23 +-
.../bos_email/src/Templates/Registry.php | 20 +-
.../bos_email/src/Templates/Sanitation.php | 29 +-
.../src/Templates/TestDrupalmail.php | 4 +-
.../src/Functional/EmailControllerTest.php | 66 ++
23 files changed, 1159 insertions(+), 884 deletions(-)
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
rename docroot/modules/custom/bos_components/modules/bos_email/src/Controller/{PostmarkAPI.php => EmailController.php} (79%)
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Controller/TokenOps.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Services/TokenOps.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/tests/src/Functional/EmailControllerTest.php
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
index aee952d5e7..f31caca1f6 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -57,7 +57,7 @@ email[contact] =
###
# group: bos_email / sanitation
-# @name Sanitation on-demand: Bearer Token
+# @name Sanitation on-demand confirmation: Bearer Token
POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
@@ -81,13 +81,13 @@ Authorization: Token {{bos_email_bearer_token}}
});
client.test("Expected Response format", function () {
client.assert(response.body.status == "success", "Response status field is not 'success'");
- client.assert(response.body.response == "Message sent" || response.body.response == "Message queued", "Response message is not expected");
+ client.assert(response.body.response == "Message sent." || response.body.response == "Message queued.", "Response message is not expected");
});
%}
###
# group: bos_email / sanitation
-# @name Sanitation scheduled: Bearer Token
+# @name Sanitation scheduled reminder: Bearer Token
POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
@@ -99,7 +99,7 @@ Authorization: Token {{bos_email_bearer_token}}
"subject": "Sanitation Confirmation",
"message": "We are pleased to confirm your pickup",
"type": "reminder1",
- "senddatetime": "10/02/2024 15:00"
+ "senddatetime": "+1 week"
}
> {%
@@ -116,6 +116,7 @@ Authorization: Token {{bos_email_bearer_token}}
});
client.test("Response contains email id", function () {
client.assert(typeof response.body.id !== "undefined" && response.body.id != "", "Response does not contain an ID field");
+ client.global.set("sanitation_email_id", response.body.id);
});
%}
@@ -445,7 +446,6 @@ Content-Disposition: form-data; name="new"
// });
%}
-
###
# group: bos_email / inbound
# @name Incoming Webhook (POSTMARK)
@@ -715,6 +715,25 @@ Authorization: Token {{bos_email_bearer_token}}
// });
%}
+###
+# group: bos_email / Cancel
+# @name Cancel Scheduled Email: Bearer Token
+POST {{url}}/rest/email_cancel/sanitation
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "id": "{{sanitation_email_id}}"
+}
+
+> {%
+ client.test("Status code is 200", function () {
+ client.assert(response.status == 200, "Response HTTP Code is not 200");
+ });
+
+ client.global.clear("sanitation_email_id");
+ %}
+
###
# group: bos_email / fail-test
# @name Bad Session Token
@@ -754,7 +773,7 @@ Content-Type: application/json
Authorization: Token asdfghjklqwertyui
{
- "to_address": {{email_test_recipient}},
+ "to_address": "{{email_test_recipient}}",
"from_address": "Sanitation ",
"subject": "Sanitation Confirmation",
"message": "We are pleased to confirm your pickup",
@@ -893,3 +912,33 @@ Authorization: Token {{bos_email_bearer_token}}
client.assert(response.body.response.indexOf("email is not valid") != false, "Response does not report a bad email address");
});
%}
+
+###
+# group: bos_email / fail-test
+# @name Bad scheduled date
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "confirmation",
+ "senddatetime": "-1 days"
+}
+
+> {%
+ client.test("Status code is 400", function () {
+ client.assert(response.status == 400, "Response HTTP Code is not 400");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response - Scheduled date is in the past", function () {
+ client.assert(response.body.response == "Scheduled date is in the past.", "Response status field is not 'Scheduled date is in the past'");
+ });
+%}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
index d6f9325cc7..457e0abef6 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
@@ -1,7 +1,7 @@
bos_email.token:
path: '/rest/email_token/{operation}'
defaults:
- _controller: '\Drupal\bos_email\Controller\PostmarkAPI::token'
+ _controller: '\Drupal\bos_email\Controller\EmailController::token'
methods: [POST]
options:
no_cache: 'TRUE'
@@ -11,7 +11,17 @@ bos_email.token:
bos_email.send:
path: '/rest/email/{service}'
defaults:
- _controller: '\Drupal\bos_email\Controller\PostmarkAPI::begin'
+ _controller: '\Drupal\bos_email\Controller\EmailController::begin'
+ methods: [POST]
+ options:
+ no_cache: 'TRUE'
+ requirements:
+ _access: 'TRUE'
+
+bos_email.cancel.scheduled:
+ path: '/rest/email_cancel/{service}'
+ defaults:
+ _controller: '\Drupal\bos_email\Controller\EmailController::cancelEmail'
methods: [POST]
options:
no_cache: 'TRUE'
@@ -21,7 +31,7 @@ bos_email.send:
bos_email.send_token_session:
path: '/rest/email_session/{service}'
defaults:
- _controller: '\Drupal\bos_email\Controller\PostmarkAPI::beginSession'
+ _controller: '\Drupal\bos_email\Controller\EmailController::beginSession'
methods: [POST]
options:
no_cache: 'TRUE'
@@ -32,7 +42,7 @@ bos_email.send_token_session:
bos_email.send_legacy:
path: '/emails'
defaults:
- _controller: '\Drupal\bos_email\Controller\PostmarkAPI::begin'
+ _controller: '\Drupal\bos_email\Controller\EmailController::begin'
options:
no_cache: 'TRUE'
methods: [POST]
@@ -62,7 +72,7 @@ bos_email.newsletter_manager_legacy:
bos_email.postmark.webhook:
path: '/rest/email/postmark/{service}/{stream}'
defaults:
- _controller: '\Drupal\bos_email\Controller\PostmarkAPI::callback'
+ _controller: '\Drupal\bos_email\Controller\EmailController::callback'
methods: [POST]
options:
no_cache: 'TRUE'
@@ -72,7 +82,7 @@ bos_email.postmark.webhook:
bos_email.drupalmail.test:
path: '/rest/email/test/drupal/{service}/{tag}'
defaults:
- _controller: '\Drupal\bos_email\Controller\DrupalmailAPI::begin'
+ _controller: '\Drupal\bos_email\Services\DrupalService::begin'
methods: [POST]
options:
no_cache: 'TRUE'
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
index ccc49caa86..bd61f8da71 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
@@ -6,7 +6,7 @@
class CobEmail {
- private array $emailFields = [
+ private array $data = [
"To" => "",
"ReplyTo" => "",
"Cc" => "",
@@ -26,6 +26,7 @@ class CobEmail {
],
"endpoint" => "",
"server" => "",
+ "service" => "",
"senddatetime" => "",
];
@@ -48,6 +49,7 @@ class CobEmail {
"TextBody" => "string",
"ReplyTo" => "email",
],
+ "service" => "string",
"endpoint" => "string",
"senddatetime" => "string",
];
@@ -57,6 +59,7 @@ class CobEmail {
"From",
"endpoint",
"server",
+ "service"
];
public const FIELD_STRING = "string";
@@ -65,7 +68,6 @@ class CobEmail {
public const FIELD_HTML = "html";
public const FIELD_NUMBER = "number";
-
public const HANDLER_POSTMARK = "postmark";
public const HANDLER_DRUPALMAIL = "drupalmail";
@@ -75,7 +77,7 @@ class CobEmail {
/**
* @const An array of headers to retain when processing header arrays.
*/
- private const KEEP = [
+ private const KEEP_HEADERS = [
"Message-ID",
"References",
"In-Reply-To",
@@ -104,7 +106,7 @@ class CobEmail {
*/
public function __construct(array $data = []) {
- $this->emailFields = array_merge($this->emailFields, $data);
+ $this->data = array_merge($this->data, $data);
if (!empty($data)) {
// Only bring in fields which are pre-defined in $this->emailFields.
@@ -115,10 +117,10 @@ public function __construct(array $data = []) {
}
// Run an initial validation -but don't fail if valoidation fails (just
// set the validation_errors field).
- $this->validate($this->emailFields);
+ $this->validate($this->data);
}
- return $this->emailFields;
+ return $this->data;
}
@@ -131,12 +133,12 @@ public function __construct(array $data = []) {
* @return bool TRUE if validated, FALSE if not. If fails, then inspect
* $this->validation_errors for causes.
*/
- public function validate(array $data = []) {
+ public function validate(array $data = []):bool {
$this->validation_errors = [];
if (empty($data)) {
- $data = $this->emailFields;
+ $data = $this->data;
}
$validated = TRUE;
@@ -200,7 +202,7 @@ public function validate(array $data = []) {
*
* @return array Array of errors from last validation.
*/
- public function getValidationErrors() {
+ public function getValidationErrors(): array {
return $this->validation_errors;
}
@@ -209,7 +211,7 @@ public function getValidationErrors() {
*
* @return bool TRUE if errors else FALSE
*/
- public function hasValidationErrors() {
+ public function hasValidationErrors(): bool {
return !empty($this->validation_errors);
}
@@ -219,13 +221,13 @@ public function hasValidationErrors() {
* @param array|string|numeric $data An array of data to sanitize
* (defaults to $this->>emailFields)
*
- * @return array|mixed The sanitized array.
+ * @return array The sanitized array.
* @throws \Exception
*/
- private function sanitize($data = []) {
+ private function sanitize($data = []): array {
if (empty($data)) {
- $data = $this->emailFields;
+ $data = $this->data;
}
foreach ($data as $field => &$value) {
@@ -325,7 +327,7 @@ private function sanitizeField(string $field, $value) {
* @throws \Exception - if the type is not "string|array|email|html|number",
* or (bubbles up) if the field cannot be sanitized.
*/
- public function addField(string $field, string $type, $value = "") {
+ public function addField(string $field, string $type, $value = ""): array {
if (!array_key_exists($field, $this->fieldTypes)) {
if (!in_array($type, ["string", "array", "email", "html", "number"])) {
@@ -336,7 +338,7 @@ public function addField(string $field, string $type, $value = "") {
$this->setField($field, $value);
- return $this->emailFields;
+ return $this->data;
}
@@ -350,11 +352,11 @@ public function addField(string $field, string $type, $value = "") {
* @return array The current sanitized but unvalidated fields in the object.
* @throws \Exception (bubbles up) if the field cannot be sanitized.
*/
- public function setField(string $field, $value = "") {
+ public function setField(string $field, $value = ""): array {
- $this->emailFields[$field] = $this->sanitizeField($field, $value);
+ $this->data[$field] = $this->sanitizeField($field, $value);
- return $this->emailFields;
+ return $this->data;
}
@@ -366,7 +368,7 @@ public function setField(string $field, $value = "") {
* @return false|mixed
*/
public function getField(string $field) {
- return $this->emailFields[$field] ?? FALSE;
+ return $this->data[$field] ?? FALSE;
}
/**
@@ -379,14 +381,14 @@ public function getField(string $field) {
* @return void
* @throws \Exception If field cannot be removed b/c is required.
*/
- public function delField(string $field) {
+ public function delField(string $field): void {
if ($this->hasField($field)) {
if (in_array($field, $this->requiredFields)) {
throw new \Exception("Cannot delete required field {$field}");
}
- unset($this->emailFields[$field]);
+ unset($this->data[$field]);
if (isset($this->fieldTypes[$field])) {
unset($this->fieldTypes[$field]);
}
@@ -400,8 +402,8 @@ public function delField(string $field) {
*
* @return bool
*/
- public function hasField($field) {
- return isset($this->emailFields[$field]);
+ public function hasField($field): bool {
+ return isset($this->data[$field]);
}
/**
@@ -410,12 +412,12 @@ public function hasField($field) {
* @return array|false The emailFields currently set in the object.
* @throws \Exception If validation fails.
*/
- public function data(bool $validate = TRUE) {
+ public function data(bool $validate = TRUE): array {
if (!$validate) {
- return $this->emailFields;
+ return $this->data;
}
- if ($this->validate($this->emailFields)) {
- return $this->emailFields;
+ if ($this->validate($this->data)) {
+ return $this->data;
}
throw new \Exception("Validation errors occurred");
}
@@ -485,8 +487,9 @@ public static function decodeFakeEmail(string $email, string $domain = "", $stri
/**
* Adds an array of headers to the email object, optionally keeping just the
- * elements provided in the $keep array. Pass empty array to insert all array
- * elements in $headers.
+ * elements provided in the $keep array. If $keep = NULL, then will use the
+ * default KEEP_HEADERS constant (array). TIP: Pass $keep = [] to retain all
+ * array elements in $headers.
*
* @param \Drupal\bos_email\CobEmail $email The email object
* @param array $headers An array of headers to process
@@ -495,7 +498,7 @@ public static function decodeFakeEmail(string $email, string $domain = "", $stri
* @return void
* @throws \Exception
*/
- public function processHeaders(array $headers, array $keep = self::KEEP) {
+ public function processHeaders(array $headers, array $keep = self::KEEP_HEADERS): void {
if (!empty($keep)) {
$_headers = [];
@@ -543,16 +546,27 @@ private static function getDomain(): string {
return implode(".", array_reverse($domain));
}
+ /**
+ * Removes empty elements from the emailfields array
+ *
+ * @return void
+ * @throws \Exception
+ */
public function removeEmpty() {
- foreach($this->emailFields as $key => $value) {
+ foreach($this->data as $key => $value) {
if ($value == "" && !in_array($key, $this->requiredFields)) {
$this->delField($key);
}
}
}
- public function is_scheduled() {
- return !empty($this->emailFields["senddatetime"]);
+ /**
+ * Returns true if the emailfields["senddatetime"] field is not empty.
+ *
+ * @return bool
+ */
+ public function is_scheduled():bool {
+ return !empty($this->data["senddatetime"]);
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
deleted file mode 100644
index 0951ec010f..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/DrupalmailAPI.php
+++ /dev/null
@@ -1,251 +0,0 @@
-request = $request;
- }
-
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container) {
- // Instantiates this form class.
- return new static(
- // Load the service required to construct this class.
- $container->get('request_stack')
- );
- }
-
- /**
- * Check / set valid session token.
- *
- */
- public function token(string $operation) {
- $data = $this->request->getCurrentRequest()->get('data');
- $token = new TokenOps();
-
- if ($operation == "create") {
- $response_token = $token->tokenCreate();
-
- }
- elseif ($operation == "remove") {
- $response_token = $token->tokenRemove($data);
-
- }
- else {
- $response_token = $token->tokenGet($data);
-
- }
-
- $response = new CacheableJsonResponse($response_token);
- return $response;
- }
-
- /**
- * Load an email into the queue for later dispatch.
- *
- * @param array $data
- * The array containing the email POST data.
- */
- public function addQueueItem(array $data) {
- $queue_name = 'email_contactform';
- $queue = \Drupal::queue($queue_name);
- $queue_item_id = $queue->createItem($data);
-
- return $queue_item_id;
- }
-
- /**
- * Check the authentication key sent in the header is valid.
- *
- * @return bool
- */
- private function authenticate() {
- $email_ops = new PostmarkOps();
- return $email_ops->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
- }
-
- /**
- * Send the email via Postmark.
- *
- * @param \Drupal\bos_email\CobEmail $mailobj The email object
- *
- * @return array
- */
- private function sendEmail(CobEmail $email) {
-
- /**
- * @var $mailobj CobEmail
- */
-
- // Extract the email object, and validate.
- try {
- $mailobj = $email->data();
- }
- catch (\Exception $e) {}
-
- if ($email->hasValidationErrors()) {
- return [
- 'status' => 'failed',
- 'response' => implode(":", $email->getValidationErrors()),
- ];
-
- }
-
- /**
- * @var \Drupal\Core\Mail\MailManager $mailManager
- */
- try {
-
- // Send the email.
- $mailobj["_error_message"] = "";
- $key = "{$this->service}.{$mailobj["Tag"]}";
-
- $mailManager = \Drupal::service('plugin.manager.mail');
-
- $sent = $mailManager->mail("bos_email", $key , $mailobj["To"], "en", $mailobj, NULL, TRUE);
-
- if (!$sent || !$sent["result"]) {
- if (!empty($params["_error_message"])) {
- throw new \Exception($params["_error_message"]);
- }
- else {
- throw new \Exception("Error sending email.");
- }
- }
-
- $response_message = self::MESSAGE_SENT;
-
- }
- catch (\Exception $e) {
- try {
- $this->addQueueItem($mailobj);
- }
- catch (\Exception $ee) {
- \Drupal::logger("bos_email:DrupalmailAPI")->info("Failed to queued mail item in {$email->getField("server")}");
- return [
- 'status' => 'error',
- 'response' => "Error sending message {$e->getMessage()}, then error queueing item {$ee->getMessage()}.",
- ];
- }
-
- if (Boston::is_local()) {
- \Drupal::logger("bos_email:DrupalmailAPI")->info("Queued {$email->getField("server")}");
- }
-
- $response_message = self::MESSAGE_QUEUED;
- }
-
-
- return [
- 'status' => 'success',
- 'response' => $response_message,
- ];
-
- }
-
-
- /**
- * Begin script and API operations.
- *
- * @param string $service
- * The server being called via the endpoint uri.
- *
- * @return CacheableJsonResponse
- * The json response to send to the endpoint caller.
- */
- public function begin(string $service, string $tag) {
-
- if (Boston::is_local()) {
- \Drupal::logger("bos_email:DrupalmailAPI")
- ->info("Starts {$service} (callback)");
- }
-
- $this->service = $service;
-
- if ($this->authenticate()) {
-
- if ($this->request->getCurrentRequest()->getMethod() == "POST") {
-
- // Get the request payload.
- $emailFields = $this->request->getCurrentRequest()->getContent();
- $emailFields = (array) json_decode($emailFields);
-
- // Format the email message.
- if (class_exists("Drupal\\bos_email\\Templates\\{$service}") === TRUE) {
-
- $this->template_class = "Drupal\\bos_email\\Templates\\{$service}";
-
- $emailFields["drupal_data"] = new CobEmail([
- "server" => $this->template_class::getServerID(),
- "endpoint" => Boston::current_environment(),
- "Tag" => $tag
- ]);
-
- // Logging
- if (Boston::is_local()) {
- \Drupal::logger("bos_email:DrupalmailAPI")
- ->info("Set data {$service}:
" . json_encode($emailFields));
- }
-
- $this->template_class::formatOutboundEmail($emailFields);
- $response_array = $this->sendEmail($emailFields["drupal_data"]);
-
- if (Boston::is_local()) {
- \Drupal::logger("bos_email:DrupalmailAPI")
- ->info("Finished {$service}: " . json_encode($response_array));
- }
-
- }
-
- if (!empty($response_array)) {
- return new CacheableJsonResponse($response_array, Response::HTTP_OK);
- }
- else {
- return new CacheableJsonResponse(["error" => "Unknown"], Response::HTTP_BAD_REQUEST);
- }
-
- }
-
- }
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
similarity index 79%
rename from docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php
rename to docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
index 77f326968f..708198bca7 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkAPI.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
@@ -3,12 +3,15 @@
namespace Drupal\bos_email\Controller;
use Boston;
+use Drupal;
use Drupal\bos_email\CobEmail;
+use Drupal\bos_email\Services\TokenOps;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
+use Drupal\Core\Config\ImmutableConfig;
/**
* Postmark class for API.
@@ -25,7 +28,7 @@
* a bearer token.
*
*/
-class PostmarkAPI extends ControllerBase {
+class EmailController extends ControllerBase {
const MESSAGE_SENT = 'Message sent.';
@@ -36,6 +39,9 @@ class PostmarkAPI extends ControllerBase {
const AUTORESPONDER_SERVERNAME = 'autoresponder';
+ const EMAIL_QUEUE = "email_contactform";
+ const EMAIL_QUEUE_SCHEDULED = "scheduled_email";
+
/**
* Current request object for this class.
*
@@ -43,18 +49,22 @@ class PostmarkAPI extends ControllerBase {
*/
public $request;
+ public ImmutableConfig $config;
+
/**
* Server hosted / mapped to Postmark.
*
* @var string
*/
- public $server;
+ public $group_id;
/**
* @var boolean
*/
public $debug;
+ private $email_service;
+
private string $error = "";
/** @var \Drupal\bos_email\EmailTemplateInterface */
@@ -62,11 +72,14 @@ class PostmarkAPI extends ControllerBase {
private string $honeypot;
+ private bool $authenticated = FALSE;
+
/**
* Public construct for Request.
*/
- public function __construct(RequestStack $request) {
+ public function __construct(RequestStack $request, Drupal\Core\Config\ConfigFactory $config) {
$this->request = $request;
+ $this->config = $config->get("bos_email.settings");
}
/**
@@ -76,10 +89,15 @@ public static function create(ContainerInterface $container) {
// Instantiates this form class.
return new static(
// Load the service required to construct this class.
- $container->get('request_stack')
+ $container->get('request_stack'),
+ $container->get('config.factory')
);
}
+ /****************************
+ * ENDPOINTS
+ ****************************/
+
/**
* Check / set valid session token.
*
@@ -89,15 +107,15 @@ public function token(string $operation) {
$token = new TokenOps();
if ($operation == "create") {
- $response_token = $token->tokenCreate();
+ $response_token = $token->sessionTokenCreate();
}
elseif ($operation == "remove") {
- $response_token = $token->tokenRemove($data);
+ $response_token = $token->sessionTokenRemove($data);
}
else {
- $response_token = $token->tokenGet($data);
+ $response_token = $token->sessionTokenGet($data);
}
@@ -105,188 +123,6 @@ public function token(string $operation) {
return $response;
}
- /**
- * Load an email into the queue for later dispatch.
- *
- * @param array $data
- * The array containing the email POST data.
- */
- public function addQueueItem(array $data) {
- $queue_name = 'email_contactform';
- $queue = \Drupal::queue($queue_name);
- $queue_item_id = $queue->createItem($data);
-
- return $queue_item_id;
- }
-
- /**
- * Load an email into the queue for later dispatch.
- *
- * @param array $data
- * The array containing the email POST data.
- */
- public function addSheduledItem(array $data) {
- $queue_name = 'scheduled_email';
- $queue = \Drupal::queue($queue_name);
- $queue_item_id = $queue->createItem($data);
-
- return $queue_item_id;
- }
-
-
- /**
- * Check the authentication key sent in the header is valid.
- *
- * @return bool
- */
- private function authenticate() {
- $email_ops = new PostmarkOps();
- return $email_ops->checkAuth($this->request->getCurrentRequest()->headers->get("authorization"));
- }
-
- /**
- * Send email via Postmark API.
- *
- * @param array $emailFields
- * The array containing Postmark API needed fieds.
- */
- private function formatEmail(array &$emailFields) {
-
- // Create a nicer sender address if possible.
- $emailFields["modified_from_address"] = $emailFields["from_address"];
- if (isset($emailFields["sender"])) {
- $emailFields["modified_from_address"] = "{$emailFields["sender"]}<{$emailFields["from_address"]}>";
- }
-
- $emailFields["postmark_data"] = new CobEmail([
- "server" => $this->server,
- ]);
-
- if (isset($this->template_class)) {
- // This allows us to inject custom templates to reformat the email.
- $this->template_class::formatOutboundEmail($emailFields);
- }
-
- else {
- // No class created to template the response.
- // Create a default message for sending.
- $cobdata = $emailFields["postmark_data"];
- $cobdata->setField("endpoint", $this::POSTMARK_DEFAULT_ENDPOINT);
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["modified_from_address"]);
-
- if (isset($emailFields["template_id"])) {
- $cobdata->setField("endpoint", "https://api.postmarkapp.com/email/withTemplate");
- $cobdata->setField("TemplateID", $emailFields["template_id"]);
- $cobdata->setField("TemplateModel", [
- "Subject" => $emailFields["subject"],
- "TextBody" => $emailFields["message"],
- "ReplyTo" => $emailFields["from_address"]
- ]);
- $cobdata->delField("ReplyTo");
- $cobdata->delField("Subject");
- $cobdata->delField("TextBody");
- }
-
- else {
- $cobdata->setField("Subject", $emailFields["subject"]);
- $cobdata->setField("TextBody", $emailFields["message"]);
- $cobdata->setField("ReplyTo", $emailFields["from_address"]);
- $cobdata->delField("TemplateID");
- $cobdata->delField("TemplateModel");
- }
-
- if (!empty($emailFields['tag'])) {
- $cobdata->setField("Tag", $emailFields['tag']);
- }
-
- $emailFields["postmark_data"] = $cobdata;
-
- }
-
- // Remove empty fields here.
- $emailFields["postmark_data"]->removeEmpty();
-
- if ($this->debug) {
- try {
- $json = json_encode(@$emailFields["postmark_data"]->data());
- }
- catch(\Exception $e) {
- $json = "Error encountered {$e->getMessage} ";
- if ($emailFields["postmark_data"]->hasValidationErrors()) {
- $json .= implode(", ", $emailFields["postmark_data"]->getValidationErrors());
- }
- }
- \Drupal::logger("bos_email:PostmarkAPI")
- ->info("Email prepped {$this->server}:
" . $json);
- }
-
- // Validate the email data
- $emailFields["postmark_data"]->validate();
- if ($emailFields["postmark_data"]->hasValidationErrors()) {
- $this->error = implode(", ", $emailFields["postmark_data"]->getValidationErrors());
- return FALSE;
- }
-
- return TRUE;
-
- }
-
- /**
- * Send the email via Postmark.
- *
- * @param \Drupal\bos_email\CobEmail $mailobj The email object
- *
- * @return array
- */
- private function sendEmail(CobEmail $email) {
-
- /**
- * @var $mailobj CobEmail
- */
-
- // Extract the email object, and validate.
- try {
- $mailobj = $email->data();
- }
- catch (\Exception $e) {}
-
- if ($email->hasValidationErrors()) {
- return [
- 'status' => 'failed',
- 'response' => implode(":", $email->getValidationErrors()),
- ];
-
- }
-
- // Send the email.
- $email_ops = new PostmarkOps();
- $sent = $email_ops->sendEmail($mailobj);
-
- if (!$sent) {
- // Add email data to queue because of Postmark failure.
- $mailobj["postmark_error"] = $email_ops->error;
- $this->addQueueItem($mailobj);
-
- if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")->info("Queued {$email->getField("server")}");
- }
-
- $response_message = self::MESSAGE_QUEUED;
-
- }
- else {
- // Message was sent successfully to sender via Postmark.
- $response_message = self::MESSAGE_SENT;
- }
-
- return [
- 'status' => 'success',
- 'response' => $response_message,
- ];
-
- }
-
/**
* Begin script and API operations when a session object has been secured.
*
@@ -297,18 +133,20 @@ private function sendEmail(CobEmail $email) {
* The json response to send to the endpoint caller.
*/
public function beginSession(string $service) {
+
$token = new TokenOps();
$data = $this->request->getCurrentRequest()->get('email');
- $data_token = $token->tokenGet($data["token_session"]);
+ $data_token = $token->sessionTokenGet($data["token_session"]);
if ($data_token["token_session"]) {
// remove token session from DB to prevent reuse
- $token->tokenRemove($data["token_session"]);
+ $token->sessionTokenRemove($data["token_session"]);
+ $this->authenticated = TRUE;
// begin normal email submission
return $this->begin($service);
}
else {
- PostmarkOps::alertHandler($data, [], "", [], "sessiontoken");
+ self::alertHandler($data, [], "", [], "sessiontoken");
return new CacheableJsonResponse([
'status' => 'error',
'response' => 'invalid token',
@@ -337,15 +175,16 @@ public function begin(string $service = 'contactform') {
$service = ucwords($service);
}
- $this->server = $service;
+ $this->group_id = $service;
if (class_exists("Drupal\\bos_email\\Templates\\{$service}") === TRUE) {
$this->template_class = "Drupal\\bos_email\\Templates\\{$service}";
- $this->server = $this->template_class::getServerID();
+ $this->group_id = $this->template_class::getGroupID();
$this->honeypot = $this->template_class::getHoneypotField() ?: "";
+ $this->email_service = $this->template_class::getEmailService();
}
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")->info("Starts {$service}");
+ Drupal::logger("bos_email:EmailController")->info("Starts {$service}");
}
if ($this->request->getCurrentRequest()->getMethod() == "POST") {
@@ -367,9 +206,17 @@ public function begin(string $service = 'contactform') {
}
}
}
+
+ if (empty($payload)) {
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => "Empty payload body.",
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
// Check the honeypot if there is one.
if (!empty($this->honeypot) && !empty($payload[$this->honeypot])) {
- PostmarkOps::alertHandler($payload, [], "", [], "honeypot");
+ self::alertHandler($payload, [], "", [], "honeypot");
return new CacheableJsonResponse([
'status' => 'success',
'response' => str_replace(".", "!", self::MESSAGE_SENT),
@@ -378,7 +225,7 @@ public function begin(string $service = 'contactform') {
// Logging
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")
+ Drupal::logger("bos_email:EmailController")
->info("Set data {$service}:
" . json_encode($payload));
}
@@ -387,27 +234,25 @@ public function begin(string $service = 'contactform') {
unset($payload["token_session"]);
}
- if ($this->authenticate()) {
+ if (!$this->authenticated) {
+ $bearer_token = $this->request->getCurrentRequest()->headers->get("authorization") ?? "";
+ $this->authenticated = TokenOps::checkBearerToken($bearer_token, $this->email_service);
+ }
+
+ if ($this->authenticated) {
// Format and validate the message body.
if ($this->formatEmail($payload)) {
// Send email.
- if ($payload["postmark_data"]->is_scheduled()) {
- $item = $payload["postmark_data"]->data();
- $id = $this->addSheduledItem($item);
- if ($id && is_numeric($id)) {
- $response_array = [
- 'status' => 'success',
- 'response' => 'Message scheduled',
- 'id' => $id
- ];
- }
+ if ($payload["email_object"]->is_scheduled()) {
+ $item = $payload["email_object"]->data();
+ return $this->addSheduledItem($item);
}
else {
- $response_array = $this->sendEmail($payload["postmark_data"]);
+ $response_array = $this->sendEmail($payload["email_object"]);
}
}
else {
- PostmarkOps::alertHandler($payload, [], "", [], $this->error);
+ self::alertHandler($payload, [], "", [], $this->error);
return new CacheableJsonResponse([
'status' => 'error',
'response' => $this->error,
@@ -416,7 +261,7 @@ public function begin(string $service = 'contactform') {
}
else {
- PostmarkOps::alertHandler($payload, [], "", [], "authtoken");
+ self::alertHandler($payload, [], "", [], "authtoken");
return new CacheableJsonResponse([
'status' => 'error',
'response' => 'could not authenticate',
@@ -433,7 +278,7 @@ public function begin(string $service = 'contactform') {
// Logging
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")
+ Drupal::logger("bos_email:EmailController")
->info("Finished {$service}: " . json_encode($response_array));
}
@@ -445,6 +290,54 @@ public function begin(string $service = 'contactform') {
}
}
+ /**
+ * Cancels a previously scheduled email.
+ *
+ * @param string $service The service name, passed from endpoint.
+ *
+ * @return \Drupal\Core\Cache\CacheableJsonResponse
+ * @throws \Exception
+ */
+ public function cancelEmail(string $service): CacheableJsonResponse {
+
+ $this->debug = str_contains($this->request->getCurrentRequest()
+ ->getHttpHost(), "lndo.site");
+
+ if ($payload = $this->request->getCurrentRequest()->getContent()) {
+ $payload = json_decode($payload, TRUE);
+ }
+
+ $this->group_id = ucwords($service);
+ $this->email_service = new \Drupal\bos_email\Services\PostmarkService;
+
+ if (empty($payload)) {
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => "Empty payload body.",
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $bearer_token = $this->request->getCurrentRequest()->headers->get("authorization") ?? "";
+ if (TokenOps::checkBearerToken($bearer_token, $this->email_service)) {
+ Drupal::database()->delete("queue")
+ ->condition("name", self::EMAIL_QUEUE_SCHEDULED, "=")
+ ->condition("item_id", $payload["id"], "=")
+ ->execute();
+
+ }
+
+ else {
+ self::alertHandler($payload, [], "", [], "authtoken");
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => 'could not authenticate',
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ return new CacheableJsonResponse([], Response::HTTP_OK);
+
+ }
+
/**
* Handles incoming webhooks from PostMark.
*
@@ -715,7 +608,7 @@ public function callback(string $service, string $stream) {
$this->debug = Boston::is_local();
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")->info("Starts {$service} (callback)");
+ Drupal::logger("bos_email:EmailController")->info("Starts {$service} (callback)");
}
if ($this->request->getCurrentRequest()->getMethod() == "POST") {
@@ -729,10 +622,10 @@ public function callback(string $service, string $stream) {
$this->template_class = "Drupal\\bos_email\\Templates\\{$service}";
- $this->server = self::AUTORESPONDER_SERVERNAME;
+ $this->group_id = self::AUTORESPONDER_SERVERNAME;
$this->stream = $stream;
- $emailFields["postmark_data"] = new CobEmail([
- "server" => $this->server,
+ $emailFields["email_object"] = new CobEmail([
+ "server" => $this->group_id,
"endpoint" => self::POSTMARK_DEFAULT_ENDPOINT,
"Tag" => $this->stream
]);
@@ -741,14 +634,14 @@ public function callback(string $service, string $stream) {
// Logging
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")
+ Drupal::logger("bos_email:EmailController")
->info("Set data {$service}:
" . json_encode($emailFields));
}
- $response_array = $this->sendEmail($emailFields["postmark_data"]);
+ $response_array = $this->sendEmail($emailFields["email_object"]);
if ($this->debug) {
- \Drupal::logger("bos_email:PostmarkAPI")
+ Drupal::logger("bos_email:EmailController")
->info("Finished Callback {$service}: " . json_encode($response_array));
}
@@ -765,6 +658,312 @@ public function callback(string $service, string $stream) {
}
+ /****************************
+ * HELPERS
+ ****************************/
+
+ /**
+ * Send email via Postmark API.
+ *
+ * @param array $emailFields
+ * The array containing Postmark API needed fieds.
+ */
+ private function formatEmail(array &$emailFields) {
+
+ // Create a nicer sender address if possible.
+ $emailFields["modified_from_address"] = $emailFields["from_address"];
+ if (isset($emailFields["sender"])) {
+ $emailFields["modified_from_address"] = "{$emailFields["sender"]}<{$emailFields["from_address"]}>";
+ }
+
+ $emailFields["email_object"] = new CobEmail([
+ "server" => $this->group_id,
+ "service" => $this->email_service::class,
+ ]);
+
+ if (isset($this->template_class)) {
+ // This allows us to inject custom templates to reformat the email.
+ $this->template_class::formatOutboundEmail($emailFields);
+ }
+
+ else {
+ // No class created to template the response.
+ // Create a default message for sending.
+ $cobdata = $emailFields["email_object"];
+ $cobdata->setField("endpoint", $this::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->setField("To", $emailFields["to_address"]);
+ $cobdata->setField("From", $emailFields["modified_from_address"]);
+
+ if (isset($emailFields["template_id"])) {
+ $cobdata->setField("endpoint", "https://api.postmarkapp.com/email/withTemplate");
+ $cobdata->setField("TemplateID", $emailFields["template_id"]);
+ $cobdata->setField("TemplateModel", [
+ "Subject" => $emailFields["subject"],
+ "TextBody" => $emailFields["message"],
+ "ReplyTo" => $emailFields["from_address"]
+ ]);
+ $cobdata->delField("ReplyTo");
+ $cobdata->delField("Subject");
+ $cobdata->delField("TextBody");
+ }
+
+ else {
+ $cobdata->setField("Subject", $emailFields["subject"]);
+ $cobdata->setField("TextBody", $emailFields["message"]);
+ $cobdata->setField("ReplyTo", $emailFields["from_address"]);
+ $cobdata->delField("TemplateID");
+ $cobdata->delField("TemplateModel");
+ }
+
+ if (!empty($emailFields['tag'])) {
+ $cobdata->setField("Tag", $emailFields['tag']);
+ }
+
+ $emailFields["email_object"] = $cobdata;
+
+ }
+
+ // Remove empty fields here.
+ $emailFields["email_object"]->removeEmpty();
+
+ if ($this->debug) {
+ try {
+ $json = json_encode(@$emailFields["email_object"]->data());
+ }
+ catch(\Exception $e) {
+ $json = "Error encountered {$e->getMessage} ";
+ if ($emailFields["email_object"]->hasValidationErrors()) {
+ $json .= implode(", ", $emailFields["email_object"]->getValidationErrors());
+ }
+ }
+ Drupal::logger("bos_email:EmailController")
+ ->info("Email prepped {$this->group_id}:
" . $json);
+ }
+
+ // Validate the email data
+ $emailFields["email_object"]->validate();
+ if ($emailFields["email_object"]->hasValidationErrors()) {
+ $this->error = implode(", ", $emailFields["email_object"]->getValidationErrors());
+ return FALSE;
+ }
+
+ return TRUE;
+
+ }
+
+ /**
+ * Send the email via Postmark.
+ *
+ * @param \Drupal\bos_email\CobEmail $mailobj The email object
+ *
+ * @return array
+ */
+ private function sendEmail(CobEmail $email) {
+
+ /**
+ * @var $mailobj CobEmail
+ */
+
+ // Extract the email object, and validate.
+ try {
+ $mailobj = $email->data();
+ }
+ catch (\Exception $e) {}
+
+ if ($email->hasValidationErrors()) {
+ return [
+ 'status' => 'failed',
+ 'response' => implode(":", $email->getValidationErrors()),
+ ];
+
+ }
+
+ // Send the email.
+ try {
+ $sent = $this->email_service->sendEmail($mailobj);
+ }
+ catch (\Exception $e) {
+ Drupal::logger("bos_email:PostmarkService")
+ ->error($this->email_service->error);
+ $sent = FALSE;
+ }
+
+ if (!$sent) {
+
+ EmailController::alertHandler($mailobj, $this->email_service->response, $this->email_service->response["http_code"]);
+ Drupal::logger("bos_email:PostmarkService")
+ ->error($this->email_service->error);
+
+ // Add email data to queue because of Postmark failure.
+ $mailobj["send_error"] = $this->email_service->error;
+ $this->addQueueItem($mailobj);
+
+ Drupal::logger("bos_email:EmailController")
+ ->info("Queued {$email->getField("server")}");
+
+ $response_message = self::MESSAGE_QUEUED;
+
+ }
+ else {
+ // Message was sent successfully to sender via Postmark.
+ $response_message = self::MESSAGE_SENT;
+ }
+
+ return [
+ 'status' => 'success',
+ 'response' => $response_message,
+ ];
+
+ }
+
+ /**
+ * Load an email into the queue for later dispatch.
+ *
+ * @param array $data
+ * The array containing the email POST data.
+ */
+ public function addQueueItem(array $data) {
+
+ $queue = Drupal::queue(self::EMAIL_QUEUE);
+ $queue_item_id = $queue->createItem($data);
+
+ return $queue_item_id;
+ }
+
+ /**
+ * Load an email into the queue for later dispatch.
+ *
+ * @param array $data
+ * The array containing the email POST data.
+ */
+ public function addSheduledItem(array $data) {
+
+ // Quick bit of validation when an item is to be scheduled.
+ if (!is_numeric($data["senddatetime"])) {
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => 'Scheduled date is invalid.',
+ ], Response::HTTP_BAD_REQUEST);
+ }
+ elseif (intval($data["senddatetime"]) < strtotime("now")) {
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => 'Scheduled date is in the past.',
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $queue = Drupal::queue(self::EMAIL_QUEUE_SCHEDULED);
+ $data["senddatetime"] = intval($data["senddatetime"]);
+ $data["send_date"] = date("D, d M Y H:i", $data["senddatetime"]);
+ $queue_item_id = $queue->createItem($data);
+
+ if ($queue_item_id && is_numeric($queue_item_id)) {
+ return new CacheableJsonResponse([
+ 'status' => 'success',
+ 'response' => 'Message scheduled',
+ 'id' => $queue_item_id
+ ], Response::HTTP_OK);
+ }
+ else {
+ return [
+ 'status' => 'error',
+ 'response' => 'Could not schedule email.',
+ ];
+ }
+
+ }
+
+ public static function alertHandler($item, $response, $http_code, $config = NULL, $error = NULL) {
+
+ if (empty($config)) {
+ $config = Drupal::configFactory()->get("bos_email.settings");
+ }
+
+ $recipient = $config->get("alerts.recipient") ?? FALSE;
+ if ($recipient) {
+
+ // Catch suppressed emails at PostMark
+ if ($config->get("hardbounce.hardbounce")
+ && isset($response["ErrorCode"])
+ && strtolower($response["ErrorCode"]) == "406") {
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if ($config->get("hardbounce.recipient") ?? FALSE) {
+ // replace recipient with hardbounce recipient.
+ $recipient = $config->get("hardbounce.recipient");
+ }
+ if (!$mailManager->mail("bos_email", 'hardbounce', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+
+ // When the token passed in the header is invalid
+ elseif ($config->get("alerts.token")
+ && $error
+ && strtolower($error) == "authtoken") {
+ $item["token_type"] = "Authentication Token";
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+
+ // When session token is invalid
+ elseif ($config->get("alerts.token")
+ && $error
+ && str_contains($error, "sessiontoken")) {
+ $item["token_type"] = "Session Token";
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+
+ // When the token needed by PostMark is invalid
+ elseif ($config->get("alerts.token")
+ && $error
+ && str_contains($error, "Cannot find token")) {
+ $item["token_type"] = "PostMark Server API Token";
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if ($mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+
+ // When the honeypot is not empty.
+ elseif ($config->get("alerts.honeypot")
+ && $error
+ && strtolower($error) == "honeypot") {
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if (!$mailManager->mail("bos_email", 'alerts.honeypot', $recipient, "en", $item, NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+ }
+
+ // If no other issues, but the email failed to send.
+ if (!isset($mailManager)
+ && $config->get("monitor.all")) {
+ $recipient = $config->get("monitor.recipient") ?? FALSE;
+ if ($recipient) {
+ $mailManager = Drupal::service('plugin.manager.mail');
+ if (!$mailManager->mail("bos_email", 'monitor.all', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
+ Drupal::logger("bos_email:PostmarkService")
+ ->warning(t("Email sending from Drupal has failed."));
+ }
+ }
+
+ }
+
+ // Do dome logging if this is a local dev environment.
+ if (str_contains(Drupal::request()->getHttpHost(), "lndo.site")) {
+ Drupal::logger("bos_email:PostmarkService")
+ ->info("Email | " . json_encode($item) . " |
+
Response | " . json_encode($response ?? []) . " |
+ HTTPCode | {$http_code} |
+
");
+ }
+ }
+
protected function removeEmptyFields(CobEmail &$data): void {
foreach($data as $key => $value) {
if ($value == "") {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
deleted file mode 100644
index d9445561df..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/PostmarkOps.php
+++ /dev/null
@@ -1,228 +0,0 @@
-getVars()["auth"];
- // Fetch the token from the posted form.
- $post_token = explode("Token ",$post);
- $quad_chunk = str_split($post_token[1], 4);
-
- foreach ($quad_chunk as $item) {
- if (str_contains($token, $item)) {
- array_push($matches, $item);
- }
- }
-
- return count(array_unique($matches)) == 15;
- }
-
- /**
- * Get vars for Postmark servers.
- */
- public function getVars() {
-
- $postmark_env = [];
- if (getenv('POSTMARK_SETTINGS')) {
- $get_vars = explode(",", getenv('POSTMARK_SETTINGS'));
- foreach ($get_vars as $item) {
- $json = explode(":", $item);
- if (!empty($json[0]) && !empty($json[1])) {
- $postmark_env[$json[0]] = $json[1];
- }
- }
- }
- else {
- $postmark_env = Settings::get('postmark_settings') ?? [];
- }
-
- return $postmark_env;
- }
-
- /**
- * Send email to Postmark.
- */
- public function sendEmail($item) {
-
- // Check if we are sending out emails.
- $config = \Drupal::configFactory()->get("bos_email.settings");
- if (!$config->get("enabled")) {
- $this->error = "Emailing temporarily suspended for all PostMark emails";
- \Drupal::logger("bos_email:PostmarkOps")->error($this->error);
- return FALSE;
- }
- elseif ($item["server"] && !$config->get(strtolower($item["server"]))["enabled"]) {
- $this->error = "Emailing temporarily suspended for {$item["server"]} emails.";
- \Drupal::logger("bos_email:PostmarkOps")->error($this->error);
- return FALSE;
- }
-
- // Send emails via Postmark API and cURL.
- $item_json = json_encode($item);
-
- try {
- $server_token = $item["server"] . "_token";
- if (empty($this->getVars()[$server_token])) {
- throw new \Exception("Cannot find token for {$item['server']}");
- }
- $server_token = $this->getVars()[$server_token];
- $headers = [
- "Accept: application/json",
- "Content-Type: application/json",
- "X-Postmark-Server-Token: " . $server_token,
- ];
- if (\Drupal::request()->headers->has("X-PM-Bounce-Type")) {
- $headers[] = "X-PM-Bounce-Type: " . \Drupal::request()->headers->get("X-PM-Bounce-Type");
- }
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $item["endpoint"]);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
- curl_setopt($ch, CURLOPT_HEADER, FALSE);
- curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
- curl_setopt($ch, CURLOPT_POSTFIELDS, $item_json);
- curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
- $response_json = curl_exec($ch);
-
- if (!$response_json) {
- if ($e = curl_error($ch)) {
- throw new \Exception("Error from Curl: {$e}
PAYLOAD:{$item_json}");
- }
- else {
- throw new \Exception("No Response from PostMark.
PAYLOAD:{$item_json}");
- }
- }
- $response = json_decode($response_json, TRUE);
-
- $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-
- if ($http_code != 200) {
- $headers = json_encode($headers);
- throw new \Exception("Postmark Error {$http_code}
HEADERS: {$headers}
PAYLOAD: {$item_json}
RESPONSE:{$response_json}");
- }
-
- if (strtolower($response["ErrorCode"]) != "0") {
- $headers = json_encode($headers);
- throw new \Exception("Postmark Error Code: {$response['ErrorCode']}
HEADERS: {$headers}
PAYLOAD: {$item_json}
RESPONSE:{$response_json}");
- }
-
- return TRUE;
-
- }
- catch (Exception $e) {
- $this->error = $e->getMessage();
- $this->alertHandler($item, $response, $http_code, $config);
- \Drupal::logger("bos_email:PostmarkOps")->error($e->getMessage());
- return FALSE;
- }
- }
-
- public static function alertHandler($item, $response, $http_code, $config, $error = NULL) {
-
- if (empty($config)) {
- $config = \Drupal::configFactory()->get("bos_email.settings");
- }
-
- $recipient = $config->get("alerts.recipient") ?? FALSE;
- if ($recipient) {
-
- // Catch suppressed emails at PostMark
- if ($config->get("hardbounce.hardbounce")
- && isset($response["ErrorCode"])
- && strtolower($response["ErrorCode"]) == "406") {
- $mailManager = \Drupal::service('plugin.manager.mail');
- if ($config->get("hardbounce.recipient") ?? FALSE) {
- // replace recipient with hardbounce recipient.
- $recipient = $config->get("hardbounce.recipient");
- }
- if (!$mailManager->mail("bos_email", 'hardbounce', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
- }
- }
-
- // When the token passed in the header is invalid
- elseif ($config->get("alerts.token")
- && $error
- && strtolower($error) == "authtoken") {
- $item["token_type"] = "Authentication Token";
- $mailManager = \Drupal::service('plugin.manager.mail');
- if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
- }
- }
-
- // When session token is invalid
- elseif ($config->get("alerts.token")
- && $error
- && str_contains($error, "sessiontoken")) {
- $item["token_type"] = "Session Token";
- $mailManager = \Drupal::service('plugin.manager.mail');
- if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
- }
- }
-
- // When the token needed by PostMark is invalid
- elseif ($config->get("alerts.token")
- && $error
- && str_contains($error, "Cannot find token")) {
- $item["token_type"] = "PostMark Server API Token";
- $mailManager = \Drupal::service('plugin.manager.mail');
- if ($mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
- }
- }
-
- // When the honeypot is not empty.
- elseif ($config->get("alerts.honeypot")
- && $error
- && strtolower($error) == "honeypot") {
- $mailManager = \Drupal::service('plugin.manager.mail');
- if (!$mailManager->mail("bos_email", 'alerts.honeypot', $recipient, "en", $item, NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")->warning(t("Email sending from Drupal has failed."));
- }
- }
- }
-
- // If no other issues, but the email failed to send.
- if (!isset($mailManager)
- && $config->get("monitor.all")) {
- $recipient = $config->get("monitor.recipient") ?? FALSE;
- if ($recipient) {
- $mailManager = \Drupal::service('plugin.manager.mail');
- if (!$mailManager->mail("bos_email", 'monitor.all', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
- \Drupal::logger("bos_email:PostmarkOps")
- ->warning(t("Email sending from Drupal has failed."));
- }
- }
-
- }
-
- // Do dome logging if this is a local dev environment.
- if (str_contains(\Drupal::request()->getHttpHost(), "lndo.site")) {
- \Drupal::logger("bos_email:PostmarkOps")
- ->info("Email | " . json_encode($item) . " |
-
Response | " . json_encode($response ?? []) . " |
- HTTPCode | {$http_code} |
-
");
- }
- }
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/TokenOps.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/TokenOps.php
deleted file mode 100644
index 710294aaac..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/TokenOps.php
+++ /dev/null
@@ -1,60 +0,0 @@
-session = \Drupal::request()->getSession();
- }
-
- /**
- * Creates a session token and saves in the session object.
- *
- * @return array The new Session Token just created.
- */
- public function tokenCreate(): array {
- $date_time = \Drupal::time()->getCurrentTime();
- $this->session->set("token_session_{$date_time}", $date_time);
- return ['token_session' => $date_time];
- }
-
- /**
- * Removes the token from the session object.
- *
- * @param string $data The session Token to remove.
- *
- * @return string[] Status.
- */
- public function tokenRemove(string $data): array {
- if ($this->session->remove("token_session_{$data}") !== null) {
- return ['token_session' => "removed"];
- }
- return ['token_session' => "not found"];
- }
-
- /**
- * Returns an array with token_session indicating if it exists.
- * This is usually used to check if a session token is valid. The field
- * token_value is populated if the token is found.
- *
- * @param string $data The Session Token.
- *
- * @return array
- */
- public function tokenGet(string $data): array {
- if ($this->session->get("token_session_{$data}", FALSE)) {
- return [
- 'token_session' => TRUE,
- 'token_value' => "token_session_{$data}",
- ];
- }
- return ['token_session' => FALSE];
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
new file mode 100644
index 0000000000..13d7a41fba
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
@@ -0,0 +1,22 @@
+configFactory->get(self::getEditableConfigNames()[0]);
+
+ $service_options = [
+ "DrupalService" => "Drupal",
+ "PostmarkService" => "Postmark"
+ ];
+
$form["bos_email"] = [
'#type' => 'fieldset',
'#title' => 'City of Boston Emailer',
'#markup' => 'Fine-grain management for emails sent via City of Boston REST API.',
"#tree" => TRUE,
- "service" => [
- "#type" => "select",
- '#title' => t('Current Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => [
- "drupal" => "Drupal",
- "postmark" => "Postmark"
- ],
- '#default_value' => $config->get('service')
- ],
-
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Email Service Enabled'),
@@ -132,6 +127,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#markup' => 'Emails from the main Contact Form - when clicking on email addresses on boston.gov.',
'#collapsible' => FALSE,
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Contact Form Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $service_options,
+ '#default_value' => $config->get('contactform.service')
+ ],
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Contact Form email service enabled'),
@@ -151,6 +153,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#markup' => 'Emails from the Registry App - confirmations.',
'#collapsible' => FALSE,
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Registry Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $service_options,
+ '#default_value' => $config->get('registry.service')
+ ],
+ "template" => [
+ "#type" => "textfield",
+ '#title' => t('Default Registry Email Template'),
+ '#description' => t('The ID for the template being used -leave blank if no template is required.'),
+ '#default_value' => $config->get('registry.template')
+ ],
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Registry email service enabled'),
@@ -170,6 +185,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#markup' => 'Emails from the Commissions App.',
'#collapsible' => FALSE,
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Commissions Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $service_options,
+ '#default_value' => $config->get('commissions.service')
+ ],
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Commission email service enabled'),
@@ -189,6 +211,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#markup' => 'Emails sent from Metrolist Listing Form processes.',
'#collapsible' => FALSE,
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Metrolist Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $service_options,
+ '#default_value' => $config->get('metrolist.service')
+ ],
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Metrolist email service enabled'),
@@ -208,6 +237,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#markup' => 'Emails sent from Sanitation WebApp.',
'#collapsible' => FALSE,
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Sanitation Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $service_options,
+ '#default_value' => $config->get('sanitation.service')
+ ],
+ "template" => [
+ "#type" => "textfield",
+ '#title' => t('Default Sanitation Email Template'),
+ '#description' => t('The ID for the template being used -leave blank if no template is required.'),
+ '#default_value' => $config->get('sanitation.template')
+ ],
"enabled" => [
'#type' => 'checkbox',
'#title' => t('Sanitation email service enabled'),
@@ -235,17 +277,23 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if ($input = $form_state->getUserInput()["bos_email"]) {
$this->configFactory->getEditable(self::getEditableConfigNames()[0])
- ->set("service", $input["service"])
->set("enabled", $input["enabled"])
->set("q_enabled", $input["q_enabled"])
+ ->set("contactform.service", $input["contactform"]["service"])
->set("contactform.enabled", $input["contactform"]["enabled"] ?? 0)
->set("contactform.q_enabled", $input["contactform"]["q_enabled"] ?? 0)
+ ->set("registry.service", $input["registry"]["service"])
+ ->set("registry.template", $input["registry"]["template"])
->set("registry.enabled", $input["registry"]["enabled"] ?? 0)
->set("registry.q_enabled", $input["registry"]["q_enabled"] ?? 0)
+ ->set("commissions.service", $input["commissions"]["service"])
->set("commissions.enabled", $input["commissions"]["enabled"] ?? 0)
->set("commissions.q_enabled", $input["commissions"]["q_enabled"] ?? 0)
+ ->set("metrolist.service", $input["metrolist"]["service"])
->set("metrolist.enabled", $input["metrolist"]["enabled"] ?? 0)
->set("metrolist.q_enabled", $input["metrolist"]["q_enabled"] ?? 0)
+ ->set("sanitation.service", $input["sanitation"]["service"])
+ ->set("sanitation.template", $input["sanitation"]["template"])
->set("sanitation.enabled", $input["sanitation"]["enabled"] ?? 0)
->set("sanitation.sched_enabled", $input["sanitation"]["sched_enabled"] ?? 0)
->set("sanitation.q_enabled", $input["sanitation"]["q_enabled"] ?? 0)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
index ab0c6e9e57..c1ad970b8b 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
@@ -2,9 +2,10 @@
namespace Drupal\bos_email\Plugin\QueueWorker;
+use Drupal\bos_email\Services\DrupalService;
use Drupal\Core\Annotation\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
-use Drupal\bos_email\Controller\PostmarkOps;
+use Exception;
/**
* Processes emails through Postmark API.
@@ -36,15 +37,24 @@ public function processItem($item) {
throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
}
- if (!empty($item["postmark_error"])) {
- unset($item["postmark_error"]);
+ if (!empty($item["send_error"])) {
+ unset($item["send_error"]);
}
- $email_ops = new PostmarkOps();
- $postmark_send = $email_ops->sendEmail($item);
+ if (!empty($item["service"])) {
+ try {
+ $email_ops = new $item["service"];
+ }
+ catch (Exception $e) {}
+ }
+
+ if (!isset($email_ops) || empty($email_ops)) {
+ // Defaults to Drupal.
+ $email_ops = new DrupalService();
+ }
- if (!$postmark_send) {
- throw new \Exception("There was a problem in bos_email:PostmarkOps. {$email_ops->error}");
+ if (!$email_ops->sendEmail($item)) {
+ throw new \Exception("There was a problem in {$email_ops::class}. {$email_ops->error}");
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
index 74a9cedcbb..11fe2e3e13 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
@@ -2,9 +2,12 @@
namespace Drupal\bos_email\Plugin\QueueWorker;
+use Drupal\bos_email\Services\DrupalService;
use Drupal\Core\Annotation\QueueWorker;
+use Drupal\Core\Queue\DelayedRequeueException;
use Drupal\Core\Queue\QueueWorkerBase;
-use Drupal\bos_email\Controller\PostmarkOps;
+use Drupal\Core\Queue\RequeueException;
+use Exception;
/**
* Processes emails through Postmark API.
@@ -23,7 +26,7 @@ class ScheduledEmailProcessor extends QueueWorkerBase {
* @param mixed $item
* The item stored in the queue.
*/
- public function processItem($item) {
+ public function processItem($item): void {
try {
$config = \Drupal::configFactory()->get("bos_email.settings");
@@ -36,20 +39,48 @@ public function processItem($item) {
throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
}
- if (!empty($item["postmark_error"])) {
- unset($item["postmark_error"]);
- }
+ if (intval($item["senddatetime"]) <= strtotime("Now")) {
+ if (!empty($item["senddatetime"])) {
+ unset($item["senddatetime"]);
+ }
+ if (!empty($item["send_date"])) {
+ unset($item["send_date"]);
+ }
+
+ // load the correct email service.
+ if (!empty($item["service"])) {
+ try {
+ $email_ops = new $item["service"];
+ }
+ catch (Exception $e) {}
+ }
+
+ if (empty($email_ops)) {
+ // Defaults to Drupal so we can always send an email.
+ $email_ops = new DrupalService();
+ }
- if ($item["senddatetime"] <= strtotime("Now")) {
- $email_ops = new PostmarkOps();
- $postmark_send = $email_ops->sendEmail($item);
+ // This will throw an error if the mail does not send, and will cause
+ // the item to remain in the queue.
+ $send = $email_ops->sendEmail($item);
+
+ }
+ else {
+ // Delay the resending of this email until its scheduled time.
+ throw new DelayedRequeueException(
+ intval($item["senddatetime"]) - strtotime("Now"),
+ "Email scheduled for the future."
+ );
}
- if (!$postmark_send) {
- throw new \Exception("There was a problem in bos_email:PostmarkOps. {$email_ops->error}");
+ if (!$send) {
+ throw new \Exception("There was a problem in bos_email:PostmarkService. {$email_ops->error}");
}
}
+ catch (DelayedRequeueException $e) {
+ throw new DelayedRequeueException($e->getDelay(), $e->getMessage());
+ }
catch (\Exception $e) {
\Drupal::logger("contactform")->error($e->getMessage());
throw new \Exception($e->getMessage());
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
new file mode 100644
index 0000000000..19d898ada3
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
@@ -0,0 +1,91 @@
+service}.{$item["Tag"]}";
+
+ $mailManager = Drupal::service('plugin.manager.mail');
+
+ $sent = $mailManager->mail("bos_email", $key , $item["To"], "en", $item, NULL, TRUE);
+
+ if (!$sent || !$sent["result"]) {
+ if (!empty($params["_error_message"])) {
+ throw new \Exception($params["_error_message"]);
+ }
+ else {
+ throw new \Exception("Error sending email.");
+ }
+ }
+
+ $response_message = self::MESSAGE_SENT;
+
+ }
+ catch (\Exception $e) {
+ try {
+ $this->addQueueItem($item);
+ }
+ catch (\Exception $ee) {
+ Drupal::logger("bos_email:DrupalService")->info("Failed to queued mail item in {$item->getField("server")}");
+ return [
+ 'status' => 'error',
+ 'response' => "Error sending message {$e->getMessage()}, then error queueing item {$ee->getMessage()}.",
+ ];
+ }
+
+ if (Boston::is_local()) {
+ Drupal::logger("bos_email:DrupalService")->info("Queued {$item->getField("server")}");
+ }
+
+ $response_message = self::MESSAGE_QUEUED;
+ }
+
+
+ return [
+ 'status' => 'success',
+ 'response' => $response_message,
+ ];
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getVars(): array {
+ // TODO: Implement getVars() method.
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
new file mode 100644
index 0000000000..12e6bb677a
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
@@ -0,0 +1,105 @@
+get("bos_email.settings");
+ if (!$config->get("enabled")) {
+ $this->error = "Emailing temporarily suspended for all PostMark emails";
+ \Drupal::logger("bos_email:PostmarkService")->error($this->error);
+ return FALSE;
+ }
+ elseif ($item["server"] && !$config->get(strtolower($item["server"]))["enabled"]) {
+ $this->error = "Emailing temporarily suspended for {$item["server"]} emails.";
+ \Drupal::logger("bos_email:PostmarkService")->error($this->error);
+ return FALSE;
+ }
+
+ try {
+ $server_token = $item["server"] . "_token";
+ $server_token = $this->getVars()[$server_token];
+ if (empty($server_token)) {
+ throw new \Exception("Cannot find token for {$item['server']}");
+ }
+
+ $headers = [
+ "Accept" => "application/json",
+ "Content-Type" => "application/json",
+ "X-Postmark-Server-Token" => $server_token,
+ ];
+ if (\Drupal::request()->headers->has("X-PM-Bounce-Type")) {
+ $headers["X-PM-Bounce-Type"] = \Drupal::request()->headers->get("X-PM-Bounce-Type");
+ }
+
+ try {
+ $response = $this->post($item["endpoint"], $item, $headers);
+ }
+ catch (Exception $e) {
+ $headers = json_encode($this->request["headers"]);
+ $item = json_encode($item);
+ $response = json_encode($this->response);
+ $this->error = "Error posting to Postmark - {$e->getMessage()}";
+ throw new \Exception("Posting Error {$this->response["http_code"]}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
+ }
+
+ if (strtolower($response["ErrorCode"]) != "0") {
+ $headers = json_encode($headers);
+ $item = json_encode($item);
+ $response = json_encode($this->response);
+ $this->error = "Error code returned from Postmark - {$response["Message"]}";
+ throw new \Exception("Return Error Code: {$response['ErrorCode']}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
+ }
+
+ return TRUE;
+
+ }
+ catch (Exception $e) {
+ $this->error = $e->getMessage();
+ return FALSE;
+ }
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/TokenOps.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/TokenOps.php
new file mode 100644
index 0000000000..69dfd2aba3
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/TokenOps.php
@@ -0,0 +1,98 @@
+session = Drupal::request()->getSession();
+ }
+
+ /**
+ * Creates an expirable one-use token. The token will expire after 60 minutes.
+ *
+ * @return array The new Session Token just created.
+ */
+ public function sessionTokenCreate(): array {
+ $date_time = Drupal::time()->getCurrentTime();
+ Drupal::service("keyvalue.expirable")
+ ->get("client_rest_token")
+ ->setWithExpire("token_session_{$date_time}", $date_time, (60 * 60));
+ // $this->session->set("token_session_{$date_time}", $date_time);
+ return ['token_session' => $date_time];
+ }
+
+ /**
+ * Removes the token from the session object.
+ *
+ * @param string $data The session Token to remove.
+ *
+ * @return string[] Status.
+ */
+ public function sessionTokenRemove(string $data): array {
+ Drupal::service("keyvalue.expirable")
+ ->get("client_rest_token")
+ ->delete("token_session_{$data}");
+// if ($this->session->remove("token_session_{$data}") !== null) {
+// if ($del) {
+ return ['token_session' => "removed"];
+// }
+// return ['token_session' => "not found"];
+ }
+
+ /**
+ * Returns an array with token_session indicating if it exists.
+ * This is usually used to check if a session token is valid. The field
+ * token_value is populated if the token is found.
+ *
+ * @param string $data The Session Token.
+ *
+ * @return array
+ */
+ public function sessionTokenGet(string $data): array {
+ $result = Drupal::service("keyvalue.expirable")
+ ->get("client_rest_token")
+ ->get("token_session_{$data}") ?? FALSE;
+
+// if ($this->session->get("token_session_{$data}", FALSE)) {
+ if ($result) {
+ return [
+ 'token_session' => TRUE,
+ 'token_value' => "token_session_{$data}",
+ ];
+ }
+ return ['token_session' => FALSE];
+
+
+ }
+
+ /**
+ * Check bearer token and authenticate.
+ */
+ public static function checkBearerToken(string $bearer_token, EmailServiceInterface $email_service) {
+
+ $matches = [];
+ // Read the auth key from settings.
+ $token = $email_service->getVars()["auth"];
+ // Fetch the token from the posted form.
+ $post_token = explode("Token ",$bearer_token);
+ $quad_chunk = str_split($post_token[1], 4);
+
+ foreach ($quad_chunk as $item) {
+ if (str_contains($token, $item)) {
+ array_push($matches, $item);
+ }
+ }
+
+ return count(array_unique($matches)) == 15;
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
index b368e56968..6f77ddefc2 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
@@ -3,7 +3,8 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
use Drupal\Component\Utility\Html;
@@ -27,14 +28,14 @@ public static function formatOutboundEmail(array &$emailFields): void {
/**
* @var $cobdata CobEmail
*/
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$cobdata->setField("Tag", "Contact Form");
if (isset($emailFields["endpoint"])) {
$cobdata->addField("endpoint", "string", $emailFields["endpoint"]);
}
else {
- $cobdata->addField("endpoint", "string", PostmarkAPI::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->addField("endpoint", "string", EmailController::POSTMARK_DEFAULT_ENDPOINT);
}
self::templatePlainText($emailFields);
@@ -65,7 +66,7 @@ public static function formatOutboundEmail(array &$emailFields): void {
}
else {
// An email template is to be used.
- $cobdata->setField("endpoint", PostmarkAPI::POSTMARK_TEMPLATE_ENDPOINT);
+ $cobdata->setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
$cobdata->delField("TextBody");
$cobdata->delField("Subject");
$cobdata->delField("HtmlBody");
@@ -78,7 +79,7 @@ public static function formatOutboundEmail(array &$emailFields): void {
*/
public static function templatePlainText(&$emailFields): void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$msg = strip_tags($emailFields["message"]);
if (empty($emailFields["TemplateID"]) && empty($emailFields["template_id"])) {
@@ -113,7 +114,7 @@ public static function templateHtmlText(&$emailFields): void {
if (empty($emailFields["TemplateID"]) && empty($emailFields["template_id"])) {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$msg = Html::escape(Xss::filter($emailFields["message"]));
$msg = str_replace("\n", "
", $msg);
@@ -143,7 +144,7 @@ public static function formatInboundEmail(array &$emailFields): void {
// if ($emailFields["endpoint"]->getField("server") == "contactform"
// && str_contains($emailFields["OriginalRecipient"], "@web-inbound.boston.gov")) {
-// $server = PostmarkAPI::AUTORESPONDER_SERVERNAME;
+// $server = EmailController::AUTORESPONDER_SERVERNAME;
// }
// Find the original recipient
@@ -152,14 +153,14 @@ public static function formatInboundEmail(array &$emailFields): void {
/**
* @var $cobdata CobEmail
*/
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$original_recipient = $cobdata::decodeFakeEmail($emailFields["OriginalRecipient"]);
$cobdata->setField("To", $original_recipient);
$cobdata->setField("From", "contactform@boston.gov");
$cobdata->setField("Subject", $emailFields["Subject"]);
$cobdata->setField("HtmlBody", $emailFields["HtmlBody"]);
$cobdata->setField("TextBody", $emailFields["TextBody"]);
- $cobdata->setField("endpoint", PostmarkAPI::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->setField("endpoint", EmailController::POSTMARK_DEFAULT_ENDPOINT);
// Select Headers
$cobdata->processHeaders($emailFields["Headers"]);
@@ -179,7 +180,17 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
return "contactform";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php
index 6776259094..69dff710d9 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php
@@ -3,7 +3,8 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateInterface;
use Drupal\bos_email\EmailTemplateBase;
@@ -17,7 +18,7 @@ class MetrolistInitiationForm extends EmailTemplateBase implements EmailTemplate
*/
public static function templatePlainText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$plain_text = trim($emailFields["message"]);
$plain_text = html_entity_decode($plain_text);
@@ -45,7 +46,7 @@ public static function templatePlainText(&$emailFields):void {
*/
public static function templateHtmlText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$html = trim($emailFields["message"]);
// Replace carriage returns with html line breaks
@@ -101,10 +102,10 @@ public static function templateHtmlText(&$emailFields):void {
*/
public static function formatOutboundEmail(array &$emailFields): void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$cobdata->setField("Tag", "metrolist form initiation");
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?? PostmarkAPI::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->setField("endpoint", $emailFields["endpoint"] ?? EmailController::POSTMARK_DEFAULT_ENDPOINT);
self::templatePlainText($emailFields);
if (!empty($emailFields["useHtml"])) {
@@ -158,10 +159,20 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getGroupID(): string {
return "metrolist";
}
+ /**
+ * @inheritDoc
+ */
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
/**
* @inheritDoc
*/
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php
index f8947aa9eb..379aac7f6d 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php
@@ -3,7 +3,8 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
@@ -17,7 +18,7 @@ class MetrolistListingConfirmation extends EmailTemplateBase implements EmailTem
*/
public static function templatePlainText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$vars = self::_getRequestParams();
@@ -46,7 +47,7 @@ public static function templatePlainText(&$emailFields):void {
*/
public static function templateHtmlText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$vars = self::_getRequestParams();
@@ -100,10 +101,10 @@ public static function templateHtmlText(&$emailFields):void {
*/
public static function formatOutboundEmail(array &$emailFields): void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$cobdata->setField("Tag", "metrolist confirmation");
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?: PostmarkAPI::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->setField("endpoint", $emailFields["endpoint"] ?: EmailController::POSTMARK_DEFAULT_ENDPOINT);
self::templatePlainText($emailFields);
if (!empty($emailFields["useHtml"])) {
@@ -277,7 +278,17 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
return "metrolist";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php
index db8d09f5f3..37381cb1ce 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php
@@ -3,7 +3,8 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
@@ -17,7 +18,7 @@ class MetrolistListingNotification extends EmailTemplateBase implements EmailTem
*/
public static function templatePlainText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$vars = self::_getRequestParams();
$decisions = "";
@@ -52,7 +53,7 @@ public static function templatePlainText(&$emailFields):void {
*/
public static function templateHtmlText(&$emailFields):void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$vars = self::_getRequestParams();
@@ -112,11 +113,11 @@ public static function templateHtmlText(&$emailFields):void {
*/
public static function formatOutboundEmail(array &$emailFields): void {
- $cobdata = &$emailFields["postmark_data"];
+ $cobdata = &$emailFields["email_object"];
$cobdata->setField("Tag", "metrolist notification");
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?: PostmarkAPI::POSTMARK_DEFAULT_ENDPOINT);
+ $cobdata->setField("endpoint", $emailFields["endpoint"] ?: EmailController::POSTMARK_DEFAULT_ENDPOINT);
self::templatePlainText($emailFields);
if (!empty($emailFields["useHtml"])) {
@@ -249,7 +250,17 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
return "metrolist";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
index 6c9877613a..04b7875909 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
@@ -3,7 +3,8 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
@@ -18,8 +19,8 @@ class Registry extends EmailTemplateBase implements EmailTemplateInterface {
public static function formatOutboundEmail(array &$emailFields): void {
/** @var $cobdata \Drupal\bos_email\CobEmail */
- $cobdata = &$emailFields["postmark_data"];
- $cobdata->setField("endpoint", PostmarkAPI::POSTMARK_TEMPLATE_ENDPOINT);
+ $cobdata = &$emailFields["email_object"];
+ $cobdata->setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
// Set up the Postmark template.
$cobdata->setField("TemplateID", $emailFields["template_id"]);
@@ -74,7 +75,18 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
return "registry";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
index 28eff71628..bf0acdd0e5 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
@@ -3,9 +3,11 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\PostmarkAPI;
+use Drupal\bos_email\Controller\EmailController;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
+use Drupal\bos_email\Services\PostmarkService;
use Exception;
/**
@@ -19,17 +21,12 @@ class Sanitation extends EmailTemplateBase implements EmailTemplateInterface {
public static function formatOutboundEmail(array &$emailFields): void {
/** @var $cobdata \Drupal\bos_email\CobEmail */
- $cobdata = &$emailFields["postmark_data"];
- $cobdata->setField("endpoint", PostmarkAPI::POSTMARK_TEMPLATE_ENDPOINT);
+ $cobdata = &$emailFields["email_object"];
+ $cobdata->setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
// Set up the Postmark template.
- $template_map = [
- "confirmation" => "sani_confirm",
- "reminder1" => "sani_remind1",
- "reminder2" => "sani_remind2",
- "cancel" => "sani_cancel",
- ];
- $cobdata->setField("TemplateID", $template_map[$emailFields["type"]]);
+ $template_id = \Drupal::config("bos_email.settings")->get("sanitation.template");
+ $cobdata->setField("TemplateID", $template_id);
$cobdata->setField("TemplateModel", [
"subject" => $emailFields["subject"],
"TextBody" => $emailFields["message"],
@@ -86,7 +83,17 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getEmailService(): EmailServiceInterface {
+ $config = \Drupal::service("config.factory")->get("bos_email.settings");
+ $email_service = $config->get(self::getGroupID() . ".service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
return "sanitation";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
index 704e9696db..99a8645ea5 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
@@ -3,7 +3,7 @@
namespace Drupal\bos_email\Templates;
use Drupal\bos_email\CobEmail;
-use Drupal\bos_email\Controller\DrupalmailAPI;
+use Drupal\bos_email\Controller\DrupalService;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
@@ -61,7 +61,7 @@ public static function getHoneypotField(): string {
/**
* @inheritDoc
*/
- public static function getServerID(): string {
+ public static function getGroupID(): string {
return "drupal_mail";
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/tests/src/Functional/EmailControllerTest.php b/docroot/modules/custom/bos_components/modules/bos_email/tests/src/Functional/EmailControllerTest.php
new file mode 100644
index 0000000000..a06ff083ac
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/tests/src/Functional/EmailControllerTest.php
@@ -0,0 +1,66 @@
+client = new Client();
+ }
+
+ /**
+ * Tests bos_email.send route.
+ */
+ public function testSendEmail() {
+
+ $endpoint = "rest/email/sanitation";
+ $data = [];
+
+ $response = $this->client->post($endpoint, [
+ "headers"=>['Content-type' => 'application/json'],
+ "body" => json_encode($data),
+ ]);
+
+ // Assert the response status and other necessary aspects.
+ $this->assertEquals(200, $response->getStatusCode());
+
+ // Additional assertions.
+ }
+
+ /**
+ * Send a GET request to an external API.
+ */
+ public function getDataFromApi($endpoint) {
+ try {
+ $response = $this->client->get($endpoint);
+ $data = Json::decode($response->getBody());
+ return $data;
+ }
+ catch (RequestException $e) {
+ watchdog_exception('my_module', $e);
+ }
+
+ return [];
+ }
+}
From f565359ad9fb91095f5e448f0a924ce8f331d022 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Mon, 22 Apr 2024 15:04:59 -0400
Subject: [PATCH 03/48] DIG-4317 Fixes linting error.
---
.../modules/bos_email/src/EmailServiceInterface.php | 6 ++++++
.../src/Plugin/QueueWorker/ContactformProcessItems.php | 2 +-
.../src/Plugin/QueueWorker/ScheduledEmailProcessor.php | 3 +--
.../modules/bos_email/src/Services/DrupalService.php | 10 +++++++---
.../modules/bos_email/src/Services/PostmarkService.php | 8 +++++++-
.../modules/bos_email/src/Templates/Sanitation.php | 2 --
6 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
index 13d7a41fba..dd8b217cd0 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
@@ -4,6 +4,12 @@
interface EmailServiceInterface {
+ /**
+ * Returns the ID for this service.
+ * @return string
+ */
+ public function id():string;
+
/**
* Send email via the Service.
*
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
index c1ad970b8b..abad4676cc 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
@@ -54,7 +54,7 @@ public function processItem($item) {
}
if (!$email_ops->sendEmail($item)) {
- throw new \Exception("There was a problem in {$email_ops::class}. {$email_ops->error}");
+ throw new \Exception("There was a problem in {$email_ops->id()}. {$email_ops->error}");
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
index 11fe2e3e13..7364b11d49 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
@@ -6,7 +6,6 @@
use Drupal\Core\Annotation\QueueWorker;
use Drupal\Core\Queue\DelayedRequeueException;
use Drupal\Core\Queue\QueueWorkerBase;
-use Drupal\Core\Queue\RequeueException;
use Exception;
/**
@@ -74,7 +73,7 @@ public function processItem($item): void {
}
if (!$send) {
- throw new \Exception("There was a problem in bos_email:PostmarkService. {$email_ops->error}");
+ throw new \Exception("There was a problem in {$email_ops->id()}. {$email_ops->error}");
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
index 19d898ada3..15aa959db7 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
@@ -4,10 +4,7 @@
use Boston;
use Drupal;
-use Drupal\bos_email\CobEmail;
use Drupal\bos_email\EmailServiceInterface;
-use Drupal\Core\Cache\CacheableJsonResponse;
-use Symfony\Component\HttpFoundation\Response;
/**
* Postmark class for API.
@@ -19,6 +16,13 @@ class DrupalService implements EmailServiceInterface {
public null|string $error;
+ /**
+ * @inheritDoc
+ */
+ public function id():string {
+ return "bos_email.DrupalService";
+ }
+
/**
* Send the email via Postmark.
*
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
index 12e6bb677a..6da7f84405 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
@@ -3,7 +3,6 @@
namespace Drupal\bos_email\Services;
use Drupal\bos_core\Controllers\Curl\BosCurlControllerBase;
-use Drupal\bos_email\Controller\EmailController;
use Drupal\bos_email\EmailServiceInterface;
use Drupal\Core\Site\Settings;
use Exception;
@@ -18,6 +17,13 @@ class PostmarkService extends BosCurlControllerBase implements EmailServiceInter
public array $response;
+ /**
+ * @inheritDoc
+ */
+ public function id():string {
+ return "bos_email.PostmarkService";
+ }
+
/**
* Get vars for Postmark servers.
*/
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
index bf0acdd0e5..1c597edb74 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
@@ -2,12 +2,10 @@
namespace Drupal\bos_email\Templates;
-use Drupal\bos_email\CobEmail;
use Drupal\bos_email\Controller\EmailController;
use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\EmailTemplateBase;
use Drupal\bos_email\EmailTemplateInterface;
-use Drupal\bos_email\Services\PostmarkService;
use Exception;
/**
From 3a1f7abe8a99f26efa4c42c1ddaa3f37a16ab422 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Mon, 22 Apr 2024 16:09:50 -0400
Subject: [PATCH 04/48] DIG-4317 Removes invalid route.
---
.../modules/bos_email/bos_email.routing.yml | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
index 457e0abef6..51c0058619 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
@@ -79,15 +79,15 @@ bos_email.postmark.webhook:
requirements:
_access: 'TRUE'
-bos_email.drupalmail.test:
- path: '/rest/email/test/drupal/{service}/{tag}'
- defaults:
- _controller: '\Drupal\bos_email\Services\DrupalService::begin'
- methods: [POST]
- options:
- no_cache: 'TRUE'
- requirements:
- _access: 'TRUE'
+#bos_email.drupalmail.test:
+# path: '/rest/email/test/drupal/{service}/{tag}'
+# defaults:
+# _controller: '\Drupal\bos_email\Services\DrupalService::begin'
+# methods: [POST]
+# options:
+# no_cache: 'TRUE'
+# requirements:
+# _access: 'TRUE'
bos_email.admin.services:
path: '/admin/config/system/boston/email_services'
From 2bb3ccdf2e033e94c6a3cc5313d3a81aab006ae1 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Wed, 24 Apr 2024 17:18:09 -0400
Subject: [PATCH 05/48] DIG-4317 Adds Drupal Service, refactoring and tests.
---
.../modules/bos_email/bos_email.http | 408 +++++++++++-------
.../modules/bos_email/bos_email.module | 38 +-
.../modules/bos_email/bos_email.routing.yml | 27 +-
.../modules/bos_email/bos_email.services.yml | 25 ++
.../modules/bos_email/http-client.env.json | 11 +-
.../modules/bos_email/src/CobEmail.php | 64 ++-
.../src/Controller/EmailController.php | 258 ++++++-----
.../bos_email/src/EmailProcessorInterface.php | 129 ++++++
.../bos_email/src/EmailServiceInterface.php | 14 +
.../bos_email/src/EmailTemplateBase.php | 59 ---
.../bos_email/src/EmailTemplateInterface.php | 75 ----
.../modules/bos_email/src/Form/ConfigForm.php | 193 ++-------
.../src/Plugin/EmailProcessor/Commissions.php | 80 ++++
.../src/Plugin/EmailProcessor/Contactform.php | 207 +++++++++
.../Plugin/EmailProcessor/DefaultEmail.php | 100 +++++
.../EmailProcessor/EmailProcessorBase.php | 176 ++++++++
.../MetrolistInitiationForm.php | 125 +++---
.../MetrolistListingConfirmation.php | 78 +---
.../MetrolistListingNotification.php | 75 +---
.../src/Plugin/EmailProcessor/Registry.php | 121 ++++++
.../src/Plugin/EmailProcessor/Sanitation.php | 150 +++++++
.../bos_email/src/Services/DrupalService.php | 164 +++++--
.../src/Services/PostmarkService.php | 52 ++-
.../bos_email/src/Templates/Contactform.php | 197 ---------
.../bos_email/src/Templates/Registry.php | 100 -----
.../bos_email/src/Templates/Sanitation.php | 105 -----
.../src/Templates/TestDrupalmail.php | 68 ---
.../templates/default.body.html.twig | 28 ++
.../includes/standard_footer.html.twig | 11 +
.../includes/standard_head.html.twig | 25 ++
.../includes/standard_masthead.html.twig | 8 +
.../mimemail-message--bos-email.html.twig | 72 ++++
.../templates/sanitation.body.html.twig | 29 ++
.../bos_core/src/Event/BosCoreFormEvent.php | 94 ++++
34 files changed, 2062 insertions(+), 1304 deletions(-)
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/bos_email.services.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateBase.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateInterface.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
rename docroot/modules/custom/bos_components/modules/bos_email/src/{Templates => Plugin/EmailProcessor}/MetrolistInitiationForm.php (50%)
rename docroot/modules/custom/bos_components/modules/bos_email/src/{Templates => Plugin/EmailProcessor}/MetrolistListingConfirmation.php (74%)
rename docroot/modules/custom/bos_components/modules/bos_email/src/{Templates => Plugin/EmailProcessor}/MetrolistListingNotification.php (73%)
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
delete mode 100644 docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/default.body.html.twig
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_footer.html.twig
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_head.html.twig
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_masthead.html.twig
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/mimemail-message--bos-email.html.twig
create mode 100644 docroot/modules/custom/bos_components/modules/bos_email/templates/sanitation.body.html.twig
create mode 100644 docroot/modules/custom/bos_core/src/Event/BosCoreFormEvent.php
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
index f31caca1f6..cd43f10093 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -24,9 +24,11 @@ Authorization: Token {{bos_email_bearer_token}}
###
# group: bos_email / session token
# @name Use client-side token for Contact Form
+# NOTE: email[contact] is the honeypot and must be present but empty.
POST {{url}}/rest/email_session/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/x-www-form-urlencoded
+Force-Service: {{ force_email_service }}
email[token_session] = {{email_session_token}} &
email[to_address] = {{email_test_recipient}} &
@@ -51,7 +53,7 @@ email[contact] =
client.assert(response.body.status == "success", "Response body.status is not 'success'");
});
client.test("Response message is 'Message Sent'", function () {
- client.assert(response.body.response == "Message sent", "Response message is not 'Message sent'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
});
%}
@@ -62,6 +64,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
"to_address": "{{email_test_recipient}}",
@@ -92,6 +95,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
"to_address": "{{email_test_recipient}}",
@@ -99,7 +103,7 @@ Authorization: Token {{bos_email_bearer_token}}
"subject": "Sanitation Confirmation",
"message": "We are pleased to confirm your pickup",
"type": "reminder1",
- "senddatetime": "+1 week"
+ "senddatetime": "+5 minutes"
}
> {%
@@ -122,11 +126,12 @@ Authorization: Token {{bos_email_bearer_token}}
###
# group: bos_email / contact form
-# @name Contact Form: Bearer Token
+# @name Contact Form - Plain Text: Bearer Token
POST {{url}}/rest/email/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
--WebAppBoundary
Content-Disposition: form-data; name="email[name]"
@@ -160,32 +165,34 @@ Content-Disposition: form-data; name="email[useHtml]"
0
--WebAppBoundary
-Content-Disposition: form-data; name="emai[contact]"
+Content-Disposition: form-data; name="email[contact]"
--WebAppBoundary
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
# group: bos_email / contact form
-# @name Contact Form HTML: Bearer Token
+# @name Contact Form - HTML: Bearer Token
POST {{url}}/rest/email/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/x-www-form-urlencoded
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
email[useHtml] = 1 &
email[to_address] = {{email_test_recipient}} &
@@ -201,16 +208,17 @@ email[browser] = PostmanRuntime/7.29.2 &
email[contact] =
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -219,6 +227,7 @@ email[contact] =
POST {{url}}/rest/email/registry
Content-Type: application/x-www-form-urlencoded
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
email[to_address] = {{email_test_recipient}} &
email[from_address] = marriage@boston.gov &
@@ -229,16 +238,17 @@ email[template_id] = {{registry_template_id}} &
email[name] = Boston Resident
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -248,6 +258,7 @@ POST {{url}}/rest/email/MetrolistInitiationForm
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
--WebAppBoundary
Content-Disposition: form-data; name="email[to_address]"
@@ -280,16 +291,17 @@ Content-Disposition: form-data; name="email[useHtml]"
--WebAppBoundary
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -299,16 +311,17 @@ POST {{url}}/rest/email/MetrolistInitiationForm
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
- "email[name]": "MetroList Listing",
- "email[url]": "https:\/\/boston.lndo.site\/metrolist\/listing-request",
- "email[from_address]": "noreply@boston.gov",
- "email[sender]": "Metrolist Listing",
- "email[to_address]": "david.upton@boston.gov",
- "email[message]": "https:\/\/boston.lndo.site\/form\/metrolist-listing?token=ZeNCdhEQXryMmZXNzqKCrVpcXdKWsmaC4gNMF1tZzVo",
- "email[useHtml]": "1",
- "email[subject]": "Your requested MetroList Listing link",
+ "name": "MetroList Listing",
+ "url": "https:\/\/boston.lndo.site\/metrolist\/listing-request",
+ "from_address": "noreply@boston.gov",
+ "sender": "Metrolist Listing",
+ "to_address": "david.upton@boston.gov",
+ "message": "https:\/\/boston.lndo.site\/form\/metrolist-listing?token=ZeNCdhEQXryMmZXNzqKCrVpcXdKWsmaC4gNMF1tZzVo",
+ "useHtml": "1",
+ "subject": "Your requested MetroList Listing link",
"hidden_name": "MetroList Listing",
"hidden_subject": "Your requested MetroList Listing link",
"hidden_message": "https:\/\/boston.lndo.site\/form\/metrolist-listing?token=ZeNCdhEQXryMmZXNzqKCrVpcXdKWsmaC4gNMF1tZzVo",
@@ -316,16 +329,17 @@ Authorization: Token {{bos_email_bearer_token}}
}
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -335,6 +349,7 @@ POST {{url}}/rest/email/MetrolistListingConfirmation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
--WebAppBoundary
Content-Disposition: form-data; name="email[to_address]"
@@ -371,16 +386,17 @@ Davids Property
--WebAppBoundary
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -390,6 +406,7 @@ POST {{url}}/rest/email/MetrolistListingNotification
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
--WebAppBoundary
Content-Disposition: form-data; name="email[name]"
@@ -434,16 +451,17 @@ Content-Disposition: form-data; name="new"
--WebAppBoundary
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -451,6 +469,7 @@ Content-Disposition: form-data; name="new"
# @name Incoming Webhook (POSTMARK)
POST {{url}}/rest/email/postmark/Contactform/inbound
Content-Type: application/json
+Force-Service: PostmarkService
{
"FromName": "Info",
@@ -674,45 +693,17 @@ Content-Type: application/json
}
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
-%}
-
-###
-# group: bos_email / Drupal mail
-# @name Test Drupal Mail : : Bearer Token
-POST {{url}}/rest/email/test/drupal/TestDrupalmail/plain
-Content-Type: application/json
-Authorization: Token {{bos_email_bearer_token}}
-
-{
- "To": "{{email_test_recipient}}",
- "Cc": "",
- "Bcc": "",
- "Subject": "Postman: Test plaintext email",
- "TextBody": "This is some text.\nThis is more text\\nThis is a link: https://www.mass.gov/covid19 ",
- "Attachments": []
-}
-
-> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'Success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message Sent'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent.", "Response message is not 'Message sent.'");
+ });
%}
###
@@ -721,6 +712,7 @@ Authorization: Token {{bos_email_bearer_token}}
POST {{url}}/rest/email_cancel/sanitation
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
"id": "{{sanitation_email_id}}"
@@ -730,7 +722,6 @@ Authorization: Token {{bos_email_bearer_token}}
client.test("Status code is 200", function () {
client.assert(response.status == 200, "Response HTTP Code is not 200");
});
-
client.global.clear("sanitation_email_id");
%}
@@ -740,6 +731,7 @@ Authorization: Token {{bos_email_bearer_token}}
POST {{url}}/rest/email_session/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/x-www-form-urlencoded
+Force-Service: {{ force_email_service }}
email[token_session] = bad-session-token &
email[to_address] = {{email_test_recipient}} &
@@ -771,6 +763,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token asdfghjklqwertyui
+Force-Service: {{ force_email_service }}
{
"to_address": "{{email_test_recipient}}",
@@ -793,12 +786,113 @@ Authorization: Token asdfghjklqwertyui
});
%}
+###
+# group: bos_email / fail-test
+# @name Bad Scheduled Date: Bearer Token
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "reminder1",
+ "senddatetime": "not a date"
+}
+
+> {%
+ client.test("Status code is 400", function () {
+ client.assert(response.status == 400, "Response HTTP Code is not 400");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "error", "Response status field is not 'error'");
+ client.assert(response.body.response == "Could not evaluate scheduled date.", "Response message is not 'Could not evaluate scheduled date.'");
+ });
+ client.test("Response contains email id", function () {
+ client.assert(typeof response.body.id !== "undefined" && response.body.id != "", "Response does not contain an ID field");
+ client.global.set("sanitation_email_id", response.body.id);
+ });
+%}
+
+###
+# group: bos_email / fail-test
+# @name Past Scheduled Date: Bearer Token
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "reminder1",
+ "senddatetime": "-1 week"
+}
+
+> {%
+ client.test("Status code is 400", function () {
+ client.assert(response.status == 400, "Response HTTP Code is not 400");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "error", "Response status field is not 'error'");
+ client.assert(response.body.response == "Scheduled date is in the past.", "Response message is not 'Scheduled date is in the past.'");
+ });
+%}
+
+###
+# group: bos_email / fail-test
+# @name >400 day Scheduled Date: Bearer Token
+POST {{url}}/rest/email/sanitation
+Cookie: XDEBUG_SESSION=PHPSTORM
+Content-Type: application/json
+Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
+
+{
+ "to_address": "{{email_test_recipient}}",
+ "from_address": "Sanitation ",
+ "subject": "Sanitation Confirmation",
+ "message": "We are pleased to confirm your pickup",
+ "type": "reminder1",
+ "senddatetime": "+401 days"
+}
+
+> {%
+ client.test("Status code is 400", function () {
+ client.assert(response.status == 400, "Response HTTP Code is not 400");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "error", "Response status field is not 'error'");
+ client.assert(response.body.response == "Emails can only be scheduled up to 400 days in advance.", "Response message is not 'Emails can only be scheduled up to 400 days in advance.'");
+ });
+%}
+
###
# group: bos_email / fail-test
# @name Missing Bearer Token
POST {{url}}/rest/email/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/x-www-form-urlencoded
+Force-Service: {{ force_email_service }}
email[to_address] = {{email_test_recipient}} &
email[name] = Valid Email Recipient &
@@ -810,26 +904,27 @@ email[browser] = PostmanRuntime/7.29.2 &
email[contact] =
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 401", function () {
- // pm.response.to.have.status(401);
- // });
- // pm.test("Status is 'error'", function () {
- // pm.expect(pm.response.text()).to.include("error");
- // });
- // pm.test("Response is 'could not authenticate'", function () {
- // pm.expect(pm.response.text()).to.include("could not authenticate");
- // });
+ client.test("Status code is 401", function () {
+ client.assert(response.status == 401, "Response HTTP Code is not 401");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response - could not authenticate", function () {
+ client.assert(response.body.response == "could not authenticate", "Response status field is not 'could not authenticate'");
+ });
%}
###
# group: bos_email / fail-test
-# @name Blocked User
+# @name Blocked User: ONLY POSTMARK
POST {{url}}/rest/email/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
#X-PM-Bounce-Type: hardbounce
Content-Type: application/x-www-form-urlencoded
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: PostmarkService
email[to_address] = blocked@boston.gov &
email[name] = Blocked Email Recipient &
@@ -841,16 +936,17 @@ email[browser] = PostmanRuntime/7.29.2 &
email[contact] =
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'success'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message queued'", function () {
- // pm.expect(pm.response.text()).to.include("Message queued");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message queued.", "Response message is not 'Message queued.'");
+ });
%}
###
@@ -860,6 +956,7 @@ POST {{url}}/rest/email/contactform
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/x-www-form-urlencoded
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
email[to_address] = {{email_test_recipient}} &
email[name] = Test Person &
@@ -871,16 +968,17 @@ email[browser] = PostmanRuntime/7.29.2 &
email[contact] = should be empty
> {%
- // TODO: migrate to HTTP Client Response handler API
- // pm.test("Status code is 200", function () {
- // pm.response.to.have.status(200);
- // });
- // pm.test("Status is 'error'", function () {
- // pm.expect(pm.response.text()).to.include("success");
- // });
- // pm.test("Response is 'Message sent!'", function () {
- // pm.expect(pm.response.text()).to.include("Message sent!");
- // });
+ client.test("Status code is 200", function () {
+ client.assert(response.status ==200, "Response HTTP Code is not 200");
+ });
+ client.test("Response is json", function () {
+ var type = response.contentType.mimeType;
+ client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
+ });
+ client.test("Expected Response format", function () {
+ client.assert(response.body.status == "success", "Response status field is not 'success'");
+ client.assert(response.body.response == "Message sent!", "Response message is not 'Message sent!'");
+ });
%}
###
@@ -890,6 +988,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
"to_address": "bademail.com",
@@ -920,6 +1019,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
+Force-Service: {{ force_email_service }}
{
"to_address": "{{email_test_recipient}}",
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
index a43d15a024..6e0e117a2d 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.module
@@ -5,6 +5,27 @@
* The Base module file for bos_email module.
*/
+use Drupal\bos_email\Services\DrupalService;
+
+/**
+ * Implements hook_theme().
+ */
+function bos_email_theme($existing, $type, $theme, $path) {
+ return [
+ 'mimemail_message__bos_email' => [
+ 'base hook' => 'mimemail_message',
+ 'template' => 'mimemail-message--bos-email',
+ ]
+ ];
+}
+
+/**
+ * Implements hook_preprocess_HOOK().
+ */
+function bos_email_preprocess_mimemail_message(&$variables) {
+ return;
+}
+
/**
* Implements hook_mail().
*/
@@ -45,24 +66,11 @@ function bos_email_mail($key, &$message, $params) {
$message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
break;
- case "TestDrupalmail.plain":
default:
- $message["from"] = $params["From"];
- $message["subject"] = $params["Subject"];
- if (!empty($params["HtmlBody"])) {
- $message["body"] = [$params["HtmlBody"]];
- $params["plain"] = FALSE;
- $params["plaintext"] = $params["TextBody"];
- }
- else {
- $message["body"] = [];
- $params["plain"] = TRUE;
- $params["plaintext"] = $params["TextBody"];
- }
-
+ // This will handle templated emails.
+ DrupalService::renderEmail($params, $message);
break;
}
- $params["To"] = empty($params["To"]) ? $params["to_address"] : $params["To"];
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
index 51c0058619..1fff43b4b2 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.routing.yml
@@ -4,7 +4,7 @@ bos_email.token:
_controller: '\Drupal\bos_email\Controller\EmailController::token'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
@@ -14,7 +14,7 @@ bos_email.send:
_controller: '\Drupal\bos_email\Controller\EmailController::begin'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
@@ -24,7 +24,7 @@ bos_email.cancel.scheduled:
_controller: '\Drupal\bos_email\Controller\EmailController::cancelEmail'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
@@ -34,17 +34,16 @@ bos_email.send_token_session:
_controller: '\Drupal\bos_email\Controller\EmailController::beginSession'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
-
bos_email.send_legacy:
path: '/emails'
defaults:
_controller: '\Drupal\bos_email\Controller\EmailController::begin'
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
methods: [POST]
requirements:
_access: 'TRUE'
@@ -55,7 +54,7 @@ bos_email.newsletter_manager:
_controller: '\Drupal\bos_email\Controller\UpakneeAPI::begin'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
@@ -65,7 +64,7 @@ bos_email.newsletter_manager_legacy:
_controller: '\Drupal\bos_email\Controller\UpakneeAPI::begin'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
@@ -75,20 +74,10 @@ bos_email.postmark.webhook:
_controller: '\Drupal\bos_email\Controller\EmailController::callback'
methods: [POST]
options:
- no_cache: 'TRUE'
+ no_cache: TRUE
requirements:
_access: 'TRUE'
-#bos_email.drupalmail.test:
-# path: '/rest/email/test/drupal/{service}/{tag}'
-# defaults:
-# _controller: '\Drupal\bos_email\Services\DrupalService::begin'
-# methods: [POST]
-# options:
-# no_cache: 'TRUE'
-# requirements:
-# _access: 'TRUE'
-
bos_email.admin.services:
path: '/admin/config/system/boston/email_services'
defaults:
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.services.yml b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.services.yml
new file mode 100644
index 0000000000..7522bff84f
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.services.yml
@@ -0,0 +1,25 @@
+services:
+ bos_email.configform.contactform:
+ class: Drupal\bos_email\Plugin\EmailProcessor\Contactform
+ tags:
+ - { name: 'event_subscriber' }
+
+ bos_email.configform.registry:
+ class: Drupal\bos_email\Plugin\EmailProcessor\Registry
+ tags:
+ - { name: 'event_subscriber' }
+
+ bos_email.configform.metrolist:
+ class: Drupal\bos_email\Plugin\EmailProcessor\MetrolistInitiationForm
+ tags:
+ - { name: 'event_subscriber' }
+
+ bos_email.configform.sanitation:
+ class: Drupal\bos_email\Plugin\EmailProcessor\Sanitation
+ tags:
+ - { name: 'event_subscriber' }
+
+ bos_email.configform.commissions:
+ class: Drupal\bos_email\Plugin\EmailProcessor\Commissions
+ tags:
+ - { name: 'event_subscriber' }
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
index 3e03ceb99b..7c1239a994 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
+++ b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
@@ -4,6 +4,15 @@
"bos_email_bearer_token": "c43517a240ee61898c00600eaa775aa0d0e639322c3f275b780f66062f69",
"contact_form_session_token": "",
"registry_template_id": "31135208",
- "email_test_recipient": "david.upton@boston.gov"
+ "email_test_recipient": "david.upton@boston.gov",
+ "force_email_service": "DrupalService"
+ },
+ "CI": {
+ "url": "https://d8-ci.boston.gov",
+ "bos_email_bearer_token": "c43517a240ee61898c00600eaa775aa0d0e639322c3f275b780f66062f69",
+ "contact_form_session_token": "",
+ "registry_template_id": "31135208",
+ "email_test_recipient": "david.upton@boston.gov",
+ "force_email_service": "DrupalService"
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
index bd61f8da71..7a51a09df2 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
@@ -3,6 +3,7 @@
namespace Drupal\bos_email;
use Drupal\Component\Utility\Xss;
+use Exception;
class CobEmail {
@@ -170,7 +171,7 @@ public function validate(array $data = []):bool {
}
}
- if (empty($data["TemplateID"])) {
+ if (empty($data["TemplateID"]) && empty($data["TemplateAlias"])) {
// No template
if (empty($data["HtmlBody"]) && empty($data["TextBody"])) {
$this->validation_errors[] = "Must have Html or Text Body for email";
@@ -182,7 +183,7 @@ public function validate(array $data = []):bool {
}
}
else {
- // Using template
+ // Using template.
if (empty($data["TemplateModel"]["TextBody"])) {
$this->validation_errors[] = "Must have Text Body for templated email";
$validated = FALSE;
@@ -194,6 +195,29 @@ public function validate(array $data = []):bool {
}
+ if (isset($data["senddatetime"])) {
+ // Quick bit of validation when an item is to be scheduled.
+ if ($data["senddatetime"] === 0 || !is_numeric($data["senddatetime"])) {
+ // If the scheduled date === 0 then we could not parse a datetime from
+ // the string in the payload (in class fn setSendDate()).
+ // If it's not numeric then something went wrong somewhere.
+ $this->validation_errors[] = "Could not evaluate scheduled date.";
+ $validated = FALSE;
+ }
+ elseif (intval($data["senddatetime"]) < strtotime("now")) {
+ // Cannot schedule in the past.
+ $this->validation_errors[] = "Scheduled date is in the past.";
+ $validated = FALSE;
+ }
+ elseif (($data["senddatetime"] - strtotime("now")) > (400 * 24 * 60 * 60)) {
+ // Only allow emails to be scheduled 400 days in advance.
+ // Helps prevent date coding issues.
+ // (Still allows for an annual reminder for example).
+ $this->validation_errors[] = "Emails can only be scheduled up to 400 days in advance.";
+ $validated = FALSE;
+ }
+ }
+
return $validated;
}
@@ -354,7 +378,14 @@ public function addField(string $field, string $type, $value = ""): array {
*/
public function setField(string $field, $value = ""): array {
- $this->data[$field] = $this->sanitizeField($field, $value);
+ if ($field == "senddatetime") {
+ // Force the senddatetime field through its own function which converts
+ // the value to a unix timestamp.
+ $this->setSendDate($value);
+ }
+ else {
+ $this->data[$field] = $this->sanitizeField($field, $value);
+ }
return $this->data;
@@ -566,7 +597,32 @@ public function removeEmpty() {
* @return bool
*/
public function is_scheduled():bool {
- return !empty($this->data["senddatetime"]);
+ return isset($this->data["senddatetime"]);
+ }
+
+ /**
+ * Adds the scheduled time to the object as a valid UNIX timestamp, or 0
+ * if it could not be evaluated.
+ *
+ * @param int|string $value a unix timestamp, or a valid date string
+ *
+ * @return void
+ */
+ public function setSendDate(int|string $value) {
+ // Note: Setting 0 as the scheduled time will validate but will raise an
+ // error later.
+ try {
+ if (is_numeric($value)) {
+ $this->data["senddatetime"] = $this->sanitizeField("senddatetime", $value);
+ }
+ else {
+ $value = strtotime($value);
+ $this->data["senddatetime"] = $this->sanitizeField("senddatetime", $value ?: 0);
+ }
+ }
+ catch (Exception $e) {
+ $this->data["senddatetime"] = $this->sanitizeField("senddatetime", 0);
+ }
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
index 708198bca7..b7b8ae9512 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
@@ -34,9 +34,6 @@ class EmailController extends ControllerBase {
const MESSAGE_QUEUED = 'Message queued.';
- const POSTMARK_DEFAULT_ENDPOINT = 'https://api.postmarkapp.com/email';
- const POSTMARK_TEMPLATE_ENDPOINT = "https://api.postmarkapp.com/email/withTemplate";
-
const AUTORESPONDER_SERVERNAME = 'autoresponder';
const EMAIL_QUEUE = "email_contactform";
@@ -67,13 +64,18 @@ class EmailController extends ControllerBase {
private string $error = "";
- /** @var \Drupal\bos_email\EmailTemplateInterface */
- private $template_class;
+ /** @var \Drupal\bos_email\EmailProcessorInterface */
+ private $email_processor;
private string $honeypot;
private bool $authenticated = FALSE;
+ /**
+ * @var string $stream The Callback stream ID
+ */
+ private string $stream;
+
/**
* Public construct for Request.
*/
@@ -167,21 +169,19 @@ public function begin(string $service = 'contactform') {
$this->debug = str_contains($this->request->getCurrentRequest()
->getHttpHost(), "lndo.site");
- $response_array = [];
- if (in_array($service, ["contactform", "registry", "sanitation"])) {
- // This is done for legacy reasons (endpoint already in production and
- // in lowercase)
- $service = ucwords($service);
- }
+ // Camelcase the service name.
+ $service = $this->endpointServiceToClass($service);
- $this->group_id = $service;
- if (class_exists("Drupal\\bos_email\\Templates\\{$service}") === TRUE) {
- $this->template_class = "Drupal\\bos_email\\Templates\\{$service}";
- $this->group_id = $this->template_class::getGroupID();
- $this->honeypot = $this->template_class::getHoneypotField() ?: "";
- $this->email_service = $this->template_class::getEmailService();
+ if (class_exists("Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}") === TRUE) {
+ $this->email_processor = "Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}";
}
+ else {
+ $this->email_processor = "Drupal\\bos_email\\Plugin\\EmailProcessor\\DefaultEmail";
+ }
+ $this->group_id = $this->email_processor::getGroupID() ?? $service;
+ $this->honeypot = $this->email_processor::getHoneypotField() ?: "";
+ $this->email_service = $this->email_processor::getEmailService($this->group_id);
if ($this->debug) {
Drupal::logger("bos_email:EmailController")->info("Starts {$service}");
@@ -189,23 +189,8 @@ public function begin(string $service = 'contactform') {
if ($this->request->getCurrentRequest()->getMethod() == "POST") {
- // Get the request payload.
- if ($this->request->getCurrentRequest()->getContentTypeFormat() == "form") {
- $payload = $this->request->getCurrentRequest()->get('email');
- }
- elseif ($this->request->getCurrentRequest()->getContentTypeFormat() == "json") {
- if ($_payload = $this->request->getCurrentRequest()->getContent()) {
- $_payload = json_decode($_payload);
- foreach ($_payload as $key => $value) {
- if (str_contains($key, "email")) {
- $payload[preg_replace('~email\[(.*)\]~', '$1', $key)] = $value;
- }
- else {
- $payload[$key] = $value;
- }
- }
- }
- }
+ // Get the payload from the request.
+ $payload = $this->email_processor::fetchPayload($this->request->getCurrentRequest());
if (empty($payload)) {
return new CacheableJsonResponse([
@@ -215,7 +200,8 @@ public function begin(string $service = 'contactform') {
}
// Check the honeypot if there is one.
- if (!empty($this->honeypot) && !empty($payload[$this->honeypot])) {
+ if (!empty($this->honeypot) &&
+ (!isset($payload[$this->honeypot]) || $payload[$this->honeypot] != "")) {
self::alertHandler($payload, [], "", [], "honeypot");
return new CacheableJsonResponse([
'status' => 'success',
@@ -223,6 +209,15 @@ public function begin(string $service = 'contactform') {
], Response::HTTP_OK);
}
+ // For testing, can force bos_email to use specified service.
+ $force = $this->request->getCurrentRequest()->headers->get("force_service", FALSE) ;
+ if ($force) {
+ $svs = "Drupal\\bos_email\\Services\\{$force}";
+ if (class_exists($svs) === TRUE) {
+ $this->email_service = new $svs;
+ }
+ }
+
// Logging
if ($this->debug) {
Drupal::logger("bos_email:EmailController")
@@ -234,24 +229,38 @@ public function begin(string $service = 'contactform') {
unset($payload["token_session"]);
}
+ // Check for authentication token if a session token was not already used.
if (!$this->authenticated) {
$bearer_token = $this->request->getCurrentRequest()->headers->get("authorization") ?? "";
$this->authenticated = TokenOps::checkBearerToken($bearer_token, $this->email_service);
}
if ($this->authenticated) {
- // Format and validate the message body.
- if ($this->formatEmail($payload)) {
+
+ // Build the email object.
+ $email_object = new CobEmail([
+ "server" => $this->group_id,
+ "service" => $this->email_service::class,
+ ]);
+
+ // Customize the email object for this Processor.
+ $this->email_processor::parseEmailFields($payload, $email_object);
+ // Map the email fields in to object as needed for this email service.
+ $this->email_service->updateEmailObject($email_object);
+
+ if ($this->verifyEmailObject($email_object)) {
+
// Send email.
- if ($payload["email_object"]->is_scheduled()) {
- $item = $payload["email_object"]->data();
+ if ($email_object->is_scheduled()) {
+ $item = $email_object->data();
return $this->addSheduledItem($item);
}
else {
- $response_array = $this->sendEmail($payload["email_object"]);
+ $response_array = $this->sendEmail($email_object);
}
}
else {
+ $payload["email_object"] = $email_object;
self::alertHandler($payload, [], "", [], $this->error);
return new CacheableJsonResponse([
'status' => 'error',
@@ -304,11 +313,20 @@ public function cancelEmail(string $service): CacheableJsonResponse {
->getHttpHost(), "lndo.site");
if ($payload = $this->request->getCurrentRequest()->getContent()) {
+ // Always JSON.
$payload = json_decode($payload, TRUE);
}
- $this->group_id = ucwords($service);
- $this->email_service = new \Drupal\bos_email\Services\PostmarkService;
+ $service = $this->endpointServiceToClass($service);
+ if (class_exists("Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}") === TRUE) {
+ $this->email_processor = "Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}";
+ }
+ else {
+ $this->email_processor = "Drupal\\bos_email\\Plugin\\EmailProcessor\\DefaultEmail";
+ }
+ $this->group_id = $this->email_processor::getGroupID() ?? $service;
+ $this->honeypot = $this->email_processor::getHoneypotField() ?: "";
+ $this->email_service = $this->email_processor::getEmailService($this->group_id);
if (empty($payload)) {
return new CacheableJsonResponse([
@@ -611,38 +629,56 @@ public function callback(string $service, string $stream) {
Drupal::logger("bos_email:EmailController")->info("Starts {$service} (callback)");
}
+ $service = $this->endpointServiceToClass($service);
+
if ($this->request->getCurrentRequest()->getMethod() == "POST") {
// Get the request payload.
- $emailFields = $this->request->getCurrentRequest()->getContent();
- $emailFields = (array) json_decode($emailFields);
+ $payload = $this->request->getCurrentRequest()->getContent();
+ $payload = json_decode($payload, TRUE);
// Format the email message.
- if (class_exists("Drupal\\bos_email\\Templates\\{$service}") === TRUE) {
+ if (class_exists("Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}") === TRUE) {
- $this->template_class = "Drupal\\bos_email\\Templates\\{$service}";
+ $this->email_processor = new ("Drupal\\bos_email\\Plugin\\EmailProcessor\\{$service}");
- $this->group_id = self::AUTORESPONDER_SERVERNAME;
+ $this->group_id = $this->email_processor->getGroupID();
$this->stream = $stream;
- $emailFields["email_object"] = new CobEmail([
+ $this->email_service = $this->email_processor::getEmailService($this->group_id);
+ $email_object = new CobEmail([
"server" => $this->group_id,
- "endpoint" => self::POSTMARK_DEFAULT_ENDPOINT,
+ "service" => $this->email_service::class,
+ "endpoint" => $this->email_service::DEFAULT_ENDPOINT,
"Tag" => $this->stream
]);
- $this->template_class::formatInboundEmail($emailFields);
+ $this->email_processor::formatInboundEmail($payload, $email_object);
+ $this->email_service->updateEmailObject($email_object);
// Logging
if ($this->debug) {
+ $payload["email_object"] = $email_object;
Drupal::logger("bos_email:EmailController")
- ->info("Set data {$service}:
" . json_encode($emailFields));
+ ->info("Set data {$service}:
" . json_encode($payload));
}
- $response_array = $this->sendEmail($emailFields["email_object"]);
+ if ($this->verifyEmailObject($email_object)) {
+
+ $response_array = $this->sendEmail($email_object);
+
+ if ($this->debug) {
+ Drupal::logger("bos_email:EmailController")
+ ->info("Finished Callback {$service}: " . json_encode($response_array));
+ }
- if ($this->debug) {
- Drupal::logger("bos_email:EmailController")
- ->info("Finished Callback {$service}: " . json_encode($response_array));
+ }
+ else {
+ $payload["email_object"] = $email_object;
+ self::alertHandler($payload, [], "", [], $this->error);
+ return new CacheableJsonResponse([
+ 'status' => 'error',
+ 'response' => $this->error,
+ ], Response::HTTP_BAD_REQUEST);
}
}
@@ -663,77 +699,24 @@ public function callback(string $service, string $stream) {
****************************/
/**
- * Send email via Postmark API.
+ * Creates the email object which will be used by the email service.
*
* @param array $emailFields
* The array containing Postmark API needed fieds.
*/
- private function formatEmail(array &$emailFields) {
-
- // Create a nicer sender address if possible.
- $emailFields["modified_from_address"] = $emailFields["from_address"];
- if (isset($emailFields["sender"])) {
- $emailFields["modified_from_address"] = "{$emailFields["sender"]}<{$emailFields["from_address"]}>";
- }
-
- $emailFields["email_object"] = new CobEmail([
- "server" => $this->group_id,
- "service" => $this->email_service::class,
- ]);
-
- if (isset($this->template_class)) {
- // This allows us to inject custom templates to reformat the email.
- $this->template_class::formatOutboundEmail($emailFields);
- }
-
- else {
- // No class created to template the response.
- // Create a default message for sending.
- $cobdata = $emailFields["email_object"];
- $cobdata->setField("endpoint", $this::POSTMARK_DEFAULT_ENDPOINT);
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["modified_from_address"]);
-
- if (isset($emailFields["template_id"])) {
- $cobdata->setField("endpoint", "https://api.postmarkapp.com/email/withTemplate");
- $cobdata->setField("TemplateID", $emailFields["template_id"]);
- $cobdata->setField("TemplateModel", [
- "Subject" => $emailFields["subject"],
- "TextBody" => $emailFields["message"],
- "ReplyTo" => $emailFields["from_address"]
- ]);
- $cobdata->delField("ReplyTo");
- $cobdata->delField("Subject");
- $cobdata->delField("TextBody");
- }
-
- else {
- $cobdata->setField("Subject", $emailFields["subject"]);
- $cobdata->setField("TextBody", $emailFields["message"]);
- $cobdata->setField("ReplyTo", $emailFields["from_address"]);
- $cobdata->delField("TemplateID");
- $cobdata->delField("TemplateModel");
- }
-
- if (!empty($emailFields['tag'])) {
- $cobdata->setField("Tag", $emailFields['tag']);
- }
-
- $emailFields["email_object"] = $cobdata;
-
- }
+ private function verifyEmailObject(CobEmail $email_object) {
// Remove empty fields here.
- $emailFields["email_object"]->removeEmpty();
+ $email_object->removeEmpty();
if ($this->debug) {
try {
- $json = json_encode(@$emailFields["email_object"]->data());
+ $json = json_encode(@$email_object->data());
}
catch(\Exception $e) {
- $json = "Error encountered {$e->getMessage} ";
- if ($emailFields["email_object"]->hasValidationErrors()) {
- $json .= implode(", ", $emailFields["email_object"]->getValidationErrors());
+ $json = "Error encountered {$e->getMessage()} ";
+ if ($email_object->hasValidationErrors()) {
+ $json .= implode(", ", $email_object->getValidationErrors());
}
}
Drupal::logger("bos_email:EmailController")
@@ -741,9 +724,9 @@ private function formatEmail(array &$emailFields) {
}
// Validate the email data
- $emailFields["email_object"]->validate();
- if ($emailFields["email_object"]->hasValidationErrors()) {
- $this->error = implode(", ", $emailFields["email_object"]->getValidationErrors());
+ $email_object->validate();
+ if ($email_object->hasValidationErrors()) {
+ $this->error = implode(", ", $email_object->getValidationErrors());
return FALSE;
}
@@ -752,7 +735,7 @@ private function formatEmail(array &$emailFields) {
}
/**
- * Send the email via Postmark.
+ * Send the email via Selected Email Service.
*
* @param \Drupal\bos_email\CobEmail $mailobj The email object
*
@@ -790,7 +773,7 @@ private function sendEmail(CobEmail $email) {
if (!$sent) {
- EmailController::alertHandler($mailobj, $this->email_service->response, $this->email_service->response["http_code"]);
+ EmailController::alertHandler($mailobj, $this->email_service->response(), $this->email_service->response()["http_code"]);
Drupal::logger("bos_email:PostmarkService")
->error($this->email_service->error);
@@ -838,20 +821,6 @@ public function addQueueItem(array $data) {
*/
public function addSheduledItem(array $data) {
- // Quick bit of validation when an item is to be scheduled.
- if (!is_numeric($data["senddatetime"])) {
- return new CacheableJsonResponse([
- 'status' => 'error',
- 'response' => 'Scheduled date is invalid.',
- ], Response::HTTP_BAD_REQUEST);
- }
- elseif (intval($data["senddatetime"]) < strtotime("now")) {
- return new CacheableJsonResponse([
- 'status' => 'error',
- 'response' => 'Scheduled date is in the past.',
- ], Response::HTTP_BAD_REQUEST);
- }
-
$queue = Drupal::queue(self::EMAIL_QUEUE_SCHEDULED);
$data["senddatetime"] = intval($data["senddatetime"]);
$data["send_date"] = date("D, d M Y H:i", $data["senddatetime"]);
@@ -972,4 +941,25 @@ protected function removeEmptyFields(CobEmail &$data): void {
}
}
+ private function endpointServiceToClass(string $service):string {
+ switch (strtolower($service)) {
+ case "metrolistinitiationform":
+ return "MetrolistInitiationForm";
+
+ case "metrolistlistingconfirmation":
+ return "MetrolistListingConfirmation";
+
+ case "metrolistlistingnotification":
+ return "MetrolistListingNotification";
+
+ case "commissions":
+ case "contactform":
+ case "registry":
+ case "sanitation":
+ default:
+ return ucwords($service);
+ }
+
+ }
+
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
new file mode 100644
index 0000000000..0dcc94cbe5
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
@@ -0,0 +1,129 @@
+addField but this
+ * should be an unusal circumstance. More commnly, when using a Template, we
+ * can set additional template arguments/parameters from the payload in this
+ * function by setting:
+ * $email_object->setField("TemplateModel", [assoc array of params]);
+ *
+ * @param array $payload The payload from the request
+ * @param CobEmail $email_object The structured email object used by mail services.
+ *
+ * @return void
+ */
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void;
+
+ /**
+ * Read request and extract the payload - output as an associative array.
+ * Typically, the code set in the EmailProcessorBase class is sufficient.
+ * Overriding this function would allow you to extract the payload from a
+ * request format/type which is not supported in the base class.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ *
+ * @return array
+ */
+ public static function fetchPayload(Request $request): array;
+
+ /**
+ * Creates a message body for incoming emails.
+ *
+ * @param array $emailFields An array containing the fields supplied from a
+ * Postmark webhook callback.
+ *
+ * @return void
+ */
+ public static function formatInboundEmail(array $payload, CobEmail &$email_object): void;
+
+ /**
+ * Creates a message body for plain text message.
+ * Adds/Updates field "TextBody" with formatted msg body to supplied array.
+ *
+ * @param array $emailFields An array containing the fields supplied from a
+ * form or the calling function needed by the template.
+ * @return void
+ */
+ public static function templatePlainText(array &$payload, CobEmail &$email_object): void;
+
+ /**
+ * Creates a message body for html message.
+ * Adds/Updates field "HtmlBody" with formatted msg body to supplied array.
+ *
+ * @param array $emailFields An array containing the fields supplied from a
+ * form or the calling function needed by the template.
+ * @return void
+ */
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object): void;
+
+ /**
+ * Returns the payload field which is a honeypot for the form submitted.
+ * NOTE: Should return "" if there is no honeypot.
+ *
+ * @return string The name of the honeypot field on the form (if any).
+ *
+ */
+ public static function getHoneypotField(): string;
+
+ /**
+ * Return the correct email service to use to relay the email.
+ * Typically, the code set in the EmailProcessorBase class is sufficient
+ * (it reads the configuration settings from the configForm).
+ * Overriding this function would allow you to specifically define a
+ * service/class in a way not supported by the base class.
+ * @see buildForm
+ * @see submitForm
+ *
+ * @param string $group_id The group ID from this Processor.
+ *
+ * @return \Drupal\bos_email\EmailServiceInterface
+ */
+ public static function getEmailService(string $group_id): EmailServiceInterface;
+
+ /**
+ * Return a group id to use in this email service.
+ *
+ * @return string The name of the groupid.
+ *
+ * This is used throughout the app and controls which outbound email server is
+ * used in Postmark - There is a token in the ENVAR POSTMARK_SETTINGS
+ * ([server]_token) which directs the email to the correct postmark server.
+ * Other email services may require a similar concept.
+ */
+ public static function getGroupID(): string;
+
+ /**
+ * Inject configuration settings into config form.
+ *
+ * @param \Drupal\bos_core\Event\BosCoreFormEvent $event
+ *
+ * @return void
+ */
+ public static function buildForm(BosCoreFormEvent $event): void;
+
+ /**
+ * Save config values from config form
+ *
+ * @param \Drupal\bos_core\Event\BosCoreFormEvent $event
+ *
+ * @return void
+ */
+ public static function submitForm(BosCoreFormEvent $event): void;
+
+ /**
+ * Validate config form before it is saved.
+ * @param \Drupal\bos_core\Event\BosCoreFormEvent $event
+ *
+ * @return void
+ */
+ public static function validateForm(BosCoreFormEvent $event): void;
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
index dd8b217cd0..9ab310a47e 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
@@ -4,12 +4,24 @@
interface EmailServiceInterface {
+ const DEFAULT_ENDPOINT = '';
+ const TEMPLATE_ENDPOINT = '';
+
/**
* Returns the ID for this service.
* @return string
*/
public function id():string;
+ /**
+ * Modify the email parameters for service requirements, e.g. templates.
+ *
+ * @param array $email_object
+ *
+ * @return void
+ */
+ public function updateEmailObject(CobEmail &$email_object): void;
+
/**
* Send email via the Service.
*
@@ -25,4 +37,6 @@ public function sendEmail(array $item): bool;
*/
public function getVars():array;
+ public function response(): array;
+
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateBase.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateBase.php
deleted file mode 100644
index 0e1935a4dc..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailTemplateBase.php
+++ /dev/null
@@ -1,59 +0,0 @@
-configFactory->get(self::getEditableConfigNames()[0]);
- $service_options = [
+ $config = $this->configFactory->getEditable(self::getEditableConfigNames()[0]);
+
+ $form["service_options"] = [
"DrupalService" => "Drupal",
"PostmarkService" => "Postmark"
];
@@ -40,12 +42,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#title' => t('Email Service Enabled'),
'#description' => t('When selected, emails will be sent via the indicated email service. When unselected all emails are added to the queue.'),
'#default_value' => $config->get('enabled'),
+ '#weight' => -10,
],
"q_enabled" => [
'#type' => 'checkbox',
'#title' => t('Email-fail Queue Enabled'),
'#description' => t('When selected, emails that the email service cannot process will be queued and there will be attempts to be resend. When unselected failed emails are discarded.'),
'#default_value' => $config->get('q_enabled'),
+ '#weight' => -9
],
"alerts" => [
@@ -53,6 +57,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#title' => 'Email Service monitoring',
'#description' => 'Configure internal alert emails for issues which arise during operations.',
'#open' => FALSE,
+ '#weight' => -8,
"conditions" => [
'#type' => 'fieldset',
@@ -121,182 +126,28 @@ public function buildForm(array $form, FormStateInterface $form_state) {
"footnote" => ['#markup' => "NOTE: These email alerts are sent via Drupal mail."],
],
- "contactform" => [
- '#type' => 'fieldset',
- '#title' => 'Contact Form',
- '#markup' => 'Emails from the main Contact Form - when clicking on email addresses on boston.gov.',
- '#collapsible' => FALSE,
-
- "service" => [
- "#type" => "select",
- '#title' => t('Contact Form Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => $service_options,
- '#default_value' => $config->get('contactform.service')
- ],
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Contact Form email service enabled'),
- '#default_value' => $config->get('contactform.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Contact Form queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
- '#default_value' => $config->get('contactform.q_enabled'),
- ],
- ],
-
- "registry" => [
- '#type' => 'fieldset',
- '#title' => 'Registry Suite',
- '#markup' => 'Emails from the Registry App - confirmations.',
- '#collapsible' => FALSE,
-
- "service" => [
- "#type" => "select",
- '#title' => t('Registry Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => $service_options,
- '#default_value' => $config->get('registry.service')
- ],
- "template" => [
- "#type" => "textfield",
- '#title' => t('Default Registry Email Template'),
- '#description' => t('The ID for the template being used -leave blank if no template is required.'),
- '#default_value' => $config->get('registry.template')
- ],
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Registry email service enabled'),
- '#default_value' => $config->get('registry.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Registry queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
- '#default_value' => $config->get('registry.q_enabled'),
- ],
- ],
-
- "commissions" => [
- '#type' => 'fieldset',
- '#title' => 'Commissions App',
- '#markup' => 'Emails from the Commissions App.',
- '#collapsible' => FALSE,
-
- "service" => [
- "#type" => "select",
- '#title' => t('Commissions Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => $service_options,
- '#default_value' => $config->get('commissions.service')
- ],
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Commission email service enabled'),
- '#default_value' => $config->get('commissions.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Commissions queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
- '#default_value' => $config->get('commissions.q_enabled'),
- ],
- ],
-
- "metrolist" => [
- '#type' => 'fieldset',
- '#title' => 'Metrolist Listing Form',
- '#markup' => 'Emails sent from Metrolist Listing Form processes.',
- '#collapsible' => FALSE,
-
- "service" => [
- "#type" => "select",
- '#title' => t('Metrolist Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => $service_options,
- '#default_value' => $config->get('metrolist.service')
- ],
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Metrolist email service enabled'),
- '#default_value' => $config->get('metrolist.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Metrolist queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
- '#default_value' => $config->get('metrolist.q_enabled'),
- ],
- ],
+ ];
- "sanitation" => [
- '#type' => 'fieldset',
- '#title' => 'Sanitation Email Services',
- '#markup' => 'Emails sent from Sanitation WebApp.',
- '#collapsible' => FALSE,
+ // Dispatch an event to form listeners so that they can add their configs
+ // to this form.
+ $event = new BosCoreFormEvent($this->getFormId(), $form, $form_state, $config);
+ $dispatcher = \Drupal::service('event_dispatcher');
+ $dispatcher->dispatch($event, BosCoreFormEvent::CONFIG_FORM_BUILD);
- "service" => [
- "#type" => "select",
- '#title' => t('Sanitation Email Service'),
- '#description' => t('The Email Service which is currently being used.'),
- "#options" => $service_options,
- '#default_value' => $config->get('sanitation.service')
- ],
- "template" => [
- "#type" => "textfield",
- '#title' => t('Default Sanitation Email Template'),
- '#description' => t('The ID for the template being used -leave blank if no template is required.'),
- '#default_value' => $config->get('sanitation.template')
- ],
- "enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Sanitation email service enabled'),
- '#default_value' => $config->get('sanitation.enabled'),
- ],
- "q_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Sanitation queue processing enabled'),
- '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
- '#default_value' => $config->get('sanitation.q_enabled'),
- ],
- "sched_enabled" => [
- '#type' => 'checkbox',
- '#title' => t('Sanitation scheduled email processing enabled'),
- '#description' => t('When selected, scheduled emails are queued will be processed on each cron run.'),
- '#default_value' => $config->get('sanitation.sched_enabled'),
- ],
- ],
+ $form = $event->getForm();
+ unset($form["service_options"]);
- ];
- return parent::buildForm($form, $form_state);
+ $form = parent::buildForm($form, $event->getFormState());
+ return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($input = $form_state->getUserInput()["bos_email"]) {
- $this->configFactory->getEditable(self::getEditableConfigNames()[0])
+ $config = $this->configFactory->getEditable(self::getEditableConfigNames()[0]);
+ $config
->set("enabled", $input["enabled"])
->set("q_enabled", $input["q_enabled"])
- ->set("contactform.service", $input["contactform"]["service"])
- ->set("contactform.enabled", $input["contactform"]["enabled"] ?? 0)
- ->set("contactform.q_enabled", $input["contactform"]["q_enabled"] ?? 0)
- ->set("registry.service", $input["registry"]["service"])
- ->set("registry.template", $input["registry"]["template"])
- ->set("registry.enabled", $input["registry"]["enabled"] ?? 0)
- ->set("registry.q_enabled", $input["registry"]["q_enabled"] ?? 0)
- ->set("commissions.service", $input["commissions"]["service"])
- ->set("commissions.enabled", $input["commissions"]["enabled"] ?? 0)
- ->set("commissions.q_enabled", $input["commissions"]["q_enabled"] ?? 0)
- ->set("metrolist.service", $input["metrolist"]["service"])
- ->set("metrolist.enabled", $input["metrolist"]["enabled"] ?? 0)
- ->set("metrolist.q_enabled", $input["metrolist"]["q_enabled"] ?? 0)
- ->set("sanitation.service", $input["sanitation"]["service"])
- ->set("sanitation.template", $input["sanitation"]["template"])
- ->set("sanitation.enabled", $input["sanitation"]["enabled"] ?? 0)
- ->set("sanitation.sched_enabled", $input["sanitation"]["sched_enabled"] ?? 0)
- ->set("sanitation.q_enabled", $input["sanitation"]["q_enabled"] ?? 0)
->set("alerts.recipient", $input["alerts"]["conditions"]["recipient"] ?? "")
->set("hardbounce.hardbounce", $input["alerts"]["hb"]["hardbounce"] ?? 0)
->set("hardbounce.recipient", $input["alerts"]["hb"]["recipient"] ?? "")
@@ -308,7 +159,11 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
->save();
}
-// parent::submitForm($form, $form_state);
+ // Dispatch an event to form listeners so that they can save their configs
+ // to this form.
+ $event = new BosCoreFormEvent($this->getFormId(), $form, $form_state, $config);
+ $dispatcher = \Drupal::service('event_dispatcher');
+ $dispatcher->dispatch($event, BosCoreFormEvent::CONFIG_FORM_SUBMIT);
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
new file mode 100644
index 0000000000..e89497e0ae
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
@@ -0,0 +1,80 @@
+ 'buildForm',
+ BosCoreFormEvent::CONFIG_FORM_SUBMIT => 'submitForm',
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "commissions";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function buildForm(BosCoreFormEvent $event): void {
+
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $form = $event->getForm();
+ $form["bos_email"]["commissions"] = [
+ '#type' => 'fieldset',
+ '#title' => 'Commissions App',
+ '#markup' => 'Emails from the Commissions App.',
+ '#collapsible' => FALSE,
+ '#weight' => 4,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Commissions Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $form["service_options"],
+ '#default_value' => $event->getConfig('commissions.service')
+ ],
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Commission email service enabled'),
+ '#default_value' => $event->getConfig('commissions.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Commissions queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('commissions.q_enabled'),
+ ],
+ ];
+
+ $event->setForm($form);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function submitForm(BosCoreFormEvent $event): void {
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $input = $event->getFormState()->getUserInput()["bos_email"];
+ $event->setConfig("commissions.service", $input["commissions"]["service"]);
+ $event->setConfig("commissions.enabled", $input["commissions"]["enabled"] ?? 0);
+ $event->setConfig("commissions.q_enabled", $input["commissions"]["q_enabled"] ?? 0);
+ }
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
new file mode 100644
index 0000000000..5e1a2d568d
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
@@ -0,0 +1,207 @@
+ 'buildForm',
+ BosCoreFormEvent::CONFIG_FORM_SUBMIT => 'submitForm',
+ ];
+ }
+
+ /**
+ * Domain to be used as the sender.
+ */
+ private const OUTBOUND_DOMAIN = "web-inbound.boston.gov";
+
+ /**
+ * @inheritDoc
+ */
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
+
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
+
+ $email_object->setField("Tag", ($payload['tag'] ?? "Contact Form"));
+
+ self::templatePlainText($payload, $email_object);
+ if (!empty($payload["useHtml"])) {
+ self::templateHtmlText($payload, $email_object);
+ }
+
+ // Create a hash of the original poster's email
+ $hashemail = $email_object::encodeFakeEmail($payload["from_address"], self::OUTBOUND_DOMAIN );
+ $email_object->setField("Metadata", [
+ "opmail" => $email_object::hashText($payload["from_address"], $email_object::ENCODE)
+ ]);
+ $email_object->setField("From", "Boston.gov Contact Form <{$hashemail}>");
+
+ isset($payload["name"]) && $email_object->setField("ReplyTo", "{$payload["name"]}<{$payload["from_address"]}>");
+ !empty($payload['headers']) && $email_object->setField("Headers", $payload['headers']);
+ empty($payload["TemplateID"]) && $email_object->setField("TemplateID", $payload["template_id"]);
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templatePlainText(array &$payload, CobEmail &$email_object): void {
+
+ $msg = strip_tags($payload["message"]);
+
+ if (empty($payload["TemplateID"]) && empty($payload["template_id"])) {
+ $text = "-- REPLY ABOVE THIS LINE -- \n\n";
+ $text .= "{$msg}\n\n";
+ $text .= "{$payload["phone"]}\n\n";
+ $text .= "-------------------------------- \n";
+ $text .= "This message was sent using the contact form on Boston.gov.";
+ $text .= " It was sent by {$payload["name"]} from {$payload["from_address"]} and {$payload["phone"]}.";
+ $text .= " It was sent from {$payload["url"]}.\n\n";
+ $text .= "-------------------------------- \n";
+ $email_object->setField("TextBody", $text);
+ }
+ else {
+ // we are using a template
+ $email_object->delField("TextBody");
+ $email_object->setField("TemplateID", $payload['TemplateID']);
+ $email_object->setField("TemplateModel", [
+ "subject" => $payload["subject"],
+ "TextBody" => $msg,
+ "ReplyTo" => $payload["from_address"],
+ ]);
+ $payload["useHtml"] = 0;
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object): void {
+
+ if (empty($payload["TemplateID"]) && empty($payload["template_id"])) {
+
+ $msg = Html::escape(Xss::filter($payload["message"]));
+ $msg = str_replace("\n", "
", $msg);
+
+ $html = "
----- REPLY ABOVE THIS LINE -----
";
+ $html .= "{$msg}
";
+ $html .= "
";
+ $html .= "{$payload["phone"]}";
+ $html .= "
";
+ $html .= " | ";
+ $html .= "This message was sent using the contact form on Boston.gov. ";
+ $html .= " It was sent by {$payload["name"]} from {$payload["from_address"]} and {$payload["phone"]}. ";
+ $html .= " It was sent from {$payload["url"]}. | ";
+ $html .= "
";
+ $html .= "
";
+
+ $email_object->setField("HtmlBody", $html);
+
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function formatInboundEmail(array $payload, CobEmail &$email_object): void {
+
+ $email_service = self::getEmailService(self::getGroupID());
+
+ // Create the email.
+ $original_recipient = $email_object::decodeFakeEmail($payload["OriginalRecipient"], self::OUTBOUND_DOMAIN);
+ $email_object->setField("To", $original_recipient);
+ $email_object->setField("From", "contactform@boston.gov");
+ $email_object->setField("Subject", $payload["Subject"]);
+ $email_object->setField("HtmlBody", $payload["HtmlBody"]);
+ $email_object->setField("TextBody", $payload["TextBody"]);
+ $email_object->setField("endpoint", $email_service::DEFAULT_ENDPOINT);
+ // Select Headers
+ $email_object->processHeaders($payload["Headers"]);
+
+ // Remove redundant fields
+ $email_object->delField("TemplateModel");
+ $email_object->delField("TemplateID");
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getHoneypotField(): string {
+ return "contact";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "contactform";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function buildForm(BosCoreFormEvent $event): void {
+
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $form = $event->getForm();
+ $form["bos_email"]["contactform"] = [
+ '#type' => 'fieldset',
+ '#title' => 'Contact Form',
+ '#markup' => 'Emails from the main Contact Form - when clicking on email addresses on boston.gov.',
+ '#collapsible' => FALSE,
+ '#weight' => 0,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Contact Form Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $form["service_options"],
+ '#default_value' => $event->getConfig('contactform.service')
+ ],
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Contact Form email service enabled'),
+ '#default_value' => $event->getConfig('contactform.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Contact Form queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('contactform.q_enabled'),
+ ],
+ ];
+
+ $event->setForm($form);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function submitForm(BosCoreFormEvent $event): void {
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $input = $event->getFormState()->getUserInput()["bos_email"];
+ $event->setConfig("contactform.service", $input["contactform"]["service"]);
+ $event->setConfig("contactform.enabled", $input["contactform"]["enabled"] ?? 0);
+ $event->setConfig("contactform.q_enabled", $input["contactform"]["q_enabled"] ?? 0);
+ }
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
new file mode 100644
index 0000000000..89171f79f8
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
@@ -0,0 +1,100 @@
+setField("TemplateModel", [assoc array of params]);
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templatePlainText(array &$payload, CobEmail &$email_object): void {
+
+ $msg = strip_tags($payload["message"]);
+
+ if (empty($payload["TemplateID"]) && empty($payload["template_id"])) {
+ $text = "-- REPLY ABOVE THIS LINE -- \n\n";
+ $text .= "{$msg}\n\n";
+ $text .= "{$payload["phone"]}\n\n";
+ $text .= "-------------------------------- \n";
+ $text .= "This message was sent using the contact form on Boston.gov.";
+ $text .= " It was sent by {$payload["name"]} from {$payload["from_address"]} and {$payload["phone"]}.";
+ $text .= " It was sent from {$payload["url"]}.\n\n";
+ $text .= "-------------------------------- \n";
+ $email_object->setField("TextBody", $text);
+ }
+ else {
+ // we are using a template
+ $email_object->delField("TextBody");
+ $email_object->setField("TemplateID", $payload['TemplateID']);
+ $email_object->setField("TemplateModel", [
+ "subject" => $payload["subject"],
+ "TextBody" => $msg,
+ "ReplyTo" => $payload["from_address"],
+ ]);
+ $payload["useHtml"] = 0;
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object): void {
+
+ if (empty($payload["TemplateID"]) && empty($payload["template_id"])) {
+
+ $msg = Html::escape(Xss::filter($payload["message"]));
+ $msg = str_replace("\n", "
", $msg);
+
+ $html = "
----- REPLY ABOVE THIS LINE -----
";
+ $html .= "{$msg}
";
+ $html .= "
";
+ $html .= "{$payload["phone"]}";
+ $html .= "
";
+ $html .= " | ";
+ $html .= "This message was sent using the contact form on Boston.gov. ";
+ $html .= " It was sent by {$payload["name"]} from {$payload["from_address"]} and {$payload["phone"]}. ";
+ $html .= " It was sent from {$payload["url"]}. | ";
+ $html .= "
";
+ $html .= "
";
+
+ $email_object->setField("HtmlBody", $html);
+
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "default";
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
new file mode 100644
index 0000000000..baeddebe85
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
@@ -0,0 +1,176 @@
+get("bos_email.settings");
+ $email_service = $config->get("{$group_id}.service");
+ $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
+ return new $email_service;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
+ // Create a nicer sender address if possible.
+ if (isset($payload["sender"]) && isset($payload["from_address"])) {
+ $payload["modified_from_address"] = $payload["from_address"];
+ $payload["modified_from_address"] = "{$payload["sender"]}<{$payload["from_address"]}>";
+ }
+ // Try to map the payload fields into the mail_object.
+ $email_object->setField("Tag", ($payload['tag'] ?? ""));
+ $email_object->setField("To", ($payload["to_address"] ?? ($payload["to"] ?? ($payload["recipient"] ?? ""))));
+ $email_object->setField("From", ($payload["modified_from_address"] ?? ($payload["from_address"] ?? "")));
+ $email_object->setField("TextBody", ($payload["message"] ?? ($payload["body"] ?? "")));
+ $email_object->setField("ReplyTo", ($payload["reply_to"] ?? ($payload["from_address"] ?? "")));
+ !empty($payload['subject']) && $email_object->setField("Subject", $payload["subject"]);
+ !empty($payload['cc']) && $email_object->setField("Cc", $payload['cc']);
+ !empty($payload['bcc']) && $email_object->setField("Bcc", $payload['bcc']);
+ !empty($payload['headers']) && $email_object->setField("Headers", $payload['headers']);
+ }
+
+ /**
+ * Decodes the payload from the request into an associative array.
+ *
+ * @param string $payload
+ *
+ * @return void
+ */
+ public static function fetchPayload(Request $request): array {
+
+ if ($request->getContentTypeFormat() == "form") {
+ $_payload = $request->getPayload();
+ if ($_payload->has("email")) {
+ $payload = $request->get("email");
+ $_payload->remove("email");
+ }
+ $payload = array_merge(($_payload->all() ?? []), ($payload ?? []));
+ return $payload;
+ }
+ elseif ($request->getContentTypeFormat() == "json") {
+ if ($_payload = $request->getContent()) {
+ $_payload = json_decode($_payload, TRUE);
+ foreach ($_payload as $key => $value) {
+ if (str_contains($key, "email")) {
+ $payload[preg_replace('~email\[(.*)\]~', '$1', $key)] = $value;
+ }
+ else {
+ $payload[$key] = $value;
+ }
+ }
+ return $payload;
+ }
+ }
+ return [];
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function formatInboundEmail(array $payload, CobEmail &$email_object): void {}
+
+ /**
+ * @inheritDoc
+ */
+ public static function templatePlainText(array &$payload, CobEmail &$email_object): void {}
+
+ /**
+ * @inheritDoc
+ */
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object): void {}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getHoneypotField(): string {
+ return "";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "default";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function buildForm(BosCoreFormEvent $event): void {}
+
+ /**
+ * @inheritDoc
+ */
+ public static function submitForm(BosCoreFormEvent $event): void {}
+
+ /**
+ * @inheritDoc
+ */
+ public static function validateForm(BosCoreFormEvent $event): void {}
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
similarity index 50%
rename from docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php
rename to docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
index 69dff710d9..1077ac1c21 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistInitiationForm.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
@@ -1,26 +1,32 @@
'buildForm',
+ BosCoreFormEvent::CONFIG_FORM_SUBMIT => 'submitForm',
+ ];
+ }
- $cobdata = &$emailFields["email_object"];
+ /**
+ * @inheritDoc
+ */
+ public static function templatePlainText(array &$payload, CobEmail &$email_object):void {
- $plain_text = trim($emailFields["message"]);
+ $plain_text = trim($payload["message"]);
$plain_text = html_entity_decode($plain_text);
// Replace html line breaks with carriage returns
$plain_text = str_ireplace(["
", "
", ""], ["\n", "\n", "\n"], $plain_text);
@@ -32,32 +38,30 @@ public static function templatePlainText(&$emailFields):void {
Metrolist Listing Form: {$plain_text} \n
Questions? Feel free to email metrolist@boston.gov\n
--------------------------------
-This message was requested from " . urldecode($emailFields['url']) . ".
- The request was initiated by {$emailFields['to_address']}.
+This message was requested from " . urldecode($payload['url']) . ".
+ The request was initiated by {$payload['to_address']}.
--------------------------------
";
- $cobdata->setField("TextBody", $plain_text);
+ $email_object->setField("TextBody", $plain_text);
}
/**
* @inheritDoc
*/
- public static function templateHtmlText(&$emailFields):void {
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object):void {
- $cobdata = &$emailFields["email_object"];
-
- $html = trim($emailFields["message"]);
+ $html = trim($payload["message"]);
// Replace carriage returns with html line breaks
$html = str_ireplace(["\n", "\r\n"], ["
"], $html);
- // $emailFields["message"] received the link url. We can change it into a
+ // $payload["message"] received the link url. We can change it into a
// button here.
$html = "
Launch metrolist listing form
";
- $form_url = urldecode($emailFields['url']);
+ $form_url = urldecode($payload['url']);
$html = "
\n
@@ -73,7 +77,7 @@ public static function templateHtmlText(&$emailFields):void {
\n
\n
This message was requested from the Metrolist Listing service on Boston.gov.
\n
-The request was initiated by {$emailFields['to_address']}.
+The request was initiated by {$payload['to_address']}.
\n
";
@@ -93,37 +97,25 @@ public static function templateHtmlText(&$emailFields):void {
"{$css}\n",
$html);
- $cobdata->setField("HtmlBody", $html);
+ $email_object->setField("HtmlBody", $html);
}
/**
* @inheritDoc
*/
- public static function formatOutboundEmail(array &$emailFields): void {
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
- $cobdata = &$emailFields["email_object"];
- $cobdata->setField("Tag", "metrolist form initiation");
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?? EmailController::POSTMARK_DEFAULT_ENDPOINT);
+ $email_object->setField("Tag", "metrolist form initiation");
- self::templatePlainText($emailFields);
- if (!empty($emailFields["useHtml"])) {
- self::templateHtmlText($emailFields);
+ self::templatePlainText($payload, $email_object);
+ if (!empty($payload["useHtml"])) {
+ self::templateHtmlText($payload, $email_object);
}
- // Create a hash of the original poster's email
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["from_address"]);
- !empty($emailFields['cc']) && $cobdata->setField("Cc", $emailFields['cc']);
- !empty($emailFields['bcc']) && $cobdata->setField("Bcc", $emailFields['bcc']);
- $cobdata->setField("Subject", $emailFields["subject"]);
- !empty($emailFields['headers']) && $cobdata->setField("Headers", $emailFields['headers']);
-
- // Remove redundant fields
- $cobdata->delField("TemplateModel");
- $cobdata->delField("TemplateID");
-
}
/**
@@ -148,14 +140,6 @@ public static function getCss():string {
";
}
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- // TODO: Implement honeypot() method.
- return "";
- }
-
/**
* @inheritDoc
*/
@@ -166,18 +150,51 @@ public static function getGroupID(): string {
/**
* @inheritDoc
*/
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
+ public static function buildForm(BosCoreFormEvent $event): void {
+
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $form = $event->getForm();
+ $form["bos_email"]["metrolist"] = [
+ '#type' => 'fieldset',
+ '#title' => 'Metrolist Listing Form',
+ '#markup' => 'Emails sent from Metrolist Listing Form processes.',
+ '#collapsible' => FALSE,
+ '#weight' => 2,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Metrolist Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $form["service_options"],
+ '#default_value' => $event->getConfig('metrolist.service')
+ ],
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Metrolist email service enabled'),
+ '#default_value' => $event->getConfig('metrolist.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Metrolist queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('metrolist.q_enabled'),
+ ],
+ ];
+
+ $event->setForm($form);
+ }
}
/**
* @inheritDoc
*/
- public static function formatInboundEmail(array &$emailFields): void {
- // TODO: Implement incoming() method.
+ public static function submitForm(BosCoreFormEvent $event): void {
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $input = $event->getFormState()->getUserInput()["bos_email"];
+ $event->setConfig("metrolist.service", $input["metrolist"]["service"]);
+ $event->setConfig("metrolist.enabled", $input["metrolist"]["enabled"] ?? 0);
+ $event->setConfig("metrolist.q_enabled", $input["metrolist"]["q_enabled"] ?? 0);
+ }
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
similarity index 74%
rename from docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php
rename to docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
index 379aac7f6d..7351dfdb9b 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingConfirmation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
@@ -1,24 +1,19 @@
setField("TextBody", $text);
+ $email_object->setField("TextBody", $text);
}
/**
* @inheritDoc
*/
- public static function templateHtmlText(&$emailFields):void {
-
- $cobdata = &$emailFields["email_object"];
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object):void {
$vars = self::_getRequestParams();
@@ -86,43 +79,31 @@ public static function templateHtmlText(&$emailFields):void {
\n
\n
This message was sent after completing the Metrolist Listing form on Boston.gov.
\n
-The form was submitted by {$emailFields['name']} ({$emailFields['to_address']}) from the page at " . urldecode($emailFields['url']) . ".
+The form was submitted by {$payload['name']} ({$payload['to_address']}) from the page at " . urldecode($payload['url']) . ".
\n
";
- $html = self::_makeHtml($html, $emailFields["subject"]);
+ $html = self::_makeHtml($html, $payload["subject"]);
- $cobdata->setField("HtmlBody", $html);
+ $email_object->setField("HtmlBody", $html);
}
/**
* @inheritDoc
*/
- public static function formatOutboundEmail(array &$emailFields): void {
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
- $cobdata = &$emailFields["email_object"];
- $cobdata->setField("Tag", "metrolist confirmation");
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?: EmailController::POSTMARK_DEFAULT_ENDPOINT);
+ $email_object->setField("Tag", "metrolist confirmation");
- self::templatePlainText($emailFields);
- if (!empty($emailFields["useHtml"])) {
- self::templateHtmlText($emailFields);
+ self::templatePlainText($payload, $email_object);
+ if (!empty($payload["useHtml"])) {
+ self::templateHtmlText($payload, $email_object);
}
- // Create a hash of the original poster's email
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["from_address"]);
- !empty($emailFields['cc']) && $cobdata->setField("Cc", $emailFields['cc']);
- !empty($emailFields['bcc']) && $cobdata->setField("Bcc", $emailFields['bcc']);
- $cobdata->setField("Subject", $emailFields["subject"]);
- !empty($emailFields['headers']) && $cobdata->setField("Headers", $emailFields['headers']);
-
- // Remove redundant fields
- $cobdata->delField("TemplateModel");
- $cobdata->delField("TemplateID");
-
}
/**
@@ -267,24 +248,6 @@ public static function _makeHtml(string $html, string $title) {
}
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- // TODO: Implement honeypot() method.
- return "";
- }
-
- /**
- * @inheritDoc
- */
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
- }
-
/**
* @inheritDoc
*/
@@ -292,11 +255,4 @@ public static function getGroupID(): string {
return "metrolist";
}
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void {
- // TODO: Implement incoming() method.
- }
-
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
similarity index 73%
rename from docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php
rename to docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
index 37381cb1ce..10b0421131 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/MetrolistListingNotification.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
@@ -1,24 +1,18 @@
setField("TextBody", $text);
+ $email_object->setField("TextBody", $text);
}
/**
* @inheritDoc
*/
- public static function templateHtmlText(&$emailFields):void {
-
- $cobdata = &$emailFields["email_object"];
+ public static function templateHtmlText(array &$payload, CobEmail &$email_object):void {
$vars = self::_getRequestParams();
@@ -98,43 +90,32 @@ public static function templateHtmlText(&$emailFields):void {
{$weblink}\n
View Pending Development Units
\n
-This submission was made via the Metrolist Listing Form on Boston.gov.
\n\n
+This submission was made via the Metrolist Listing Form on Boston.gov.
\n\n
\n
";
- $emailFields["HtmlBody"] = self::_makeHtml($html, $emailFields["subject"]);
+ $payload["HtmlBody"] = self::_makeHtml($html, $payload["subject"]);
- $cobdata->setField("HtmlBody", $html);
+ $email_object->setField("HtmlBody", $html);
}
/**
* @inheritDoc
*/
- public static function formatOutboundEmail(array &$emailFields): void {
-
- $cobdata = &$emailFields["email_object"];
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
- $cobdata->setField("Tag", "metrolist notification");
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
- $cobdata->setField("endpoint", $emailFields["endpoint"] ?: EmailController::POSTMARK_DEFAULT_ENDPOINT);
+ $email_object->setField("Tag", "metrolist notification");
- self::templatePlainText($emailFields);
- if (!empty($emailFields["useHtml"])) {
- self::templateHtmlText($emailFields);
+ self::templatePlainText($payload, $email_object);
+ if (!empty($payload["useHtml"])) {
+ self::templateHtmlText($payload, $email_object);
}
// Create a hash of the original poster's email
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["from_address"]);
- !empty($emailFields['cc']) && $cobdata->setField("Cc", $emailFields['cc']);
- !empty($emailFields['bcc']) && $cobdata->setField("Bcc", $emailFields['bcc']);
- $cobdata->setField("Subject", $emailFields["subject"]);
- !empty($emailFields['headers']) && $cobdata->setField("Headers", $emailFields['headers']);
-
- // Remove redundant fields
- $cobdata->delField("TemplateModel");
- $cobdata->delField("TemplateID");
}
@@ -239,23 +220,6 @@ public static function _makeHtml(string $html, string $title): string {
}
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- // TODO: Implement honeypot() method.
- return "";
- }
-
- /**
- * @inheritDoc
- */
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
- }
/**
* @inheritDoc
@@ -264,11 +228,4 @@ public static function getGroupID(): string {
return "metrolist";
}
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void {
- // TODO: Implement incoming() method.
- }
-
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
new file mode 100644
index 0000000000..53fc66cade
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
@@ -0,0 +1,121 @@
+ 'buildForm',
+ BosCoreFormEvent::CONFIG_FORM_SUBMIT => 'submitForm',
+// BosCoreFormEvent::CONFIG_FORM_VALIDATE => 'validateForm'
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
+
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
+
+ $email_object->setField("TemplateID", $payload["template_id"]);
+
+ isset($emailFields["name"]) && $email_object->setField("ReplyTo", "{$payload["name"]}<{$emailFields["from_address"]}>");
+
+ // Create a relevant tag.
+ if (str_contains($payload["subject"], "Birth")) {
+ $email_object->setField("Tag", "Birth Certificate");
+ }
+ elseif (str_contains($payload["subject"], "Intention")) {
+ $email_object->setField("Tag", "Marriage Intention");
+ }
+ elseif (str_contains($payload["subject"], "Death")) {
+ $email_object->setField("Tag", "Death Certificate");
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getHoneypotField(): string {
+ return "";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "registry";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function buildForm(BosCoreFormEvent $event): void {
+
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $form = $event->getForm();
+ $form["bos_email"]["registry"] = [
+ '#type' => 'fieldset',
+ '#title' => 'Registry Suite',
+ '#markup' => 'Emails from the Registry App - confirmations.',
+ '#collapsible' => FALSE,
+ '#weight' => 1,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Registry Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $form["service_options"],
+ '#default_value' => $event->getConfig('registry.service')
+ ],
+ "template" => [
+ "#type" => "textfield",
+ '#title' => t('Default Registry Email Template'),
+ '#description' => t('The ID for the template being used -leave blank if no template is required.'),
+ '#default_value' => $event->getConfig('registry.template')
+ ],
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Registry email service enabled'),
+ '#default_value' => $event->getConfig('registry.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Registry queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('registry.q_enabled'),
+ ],
+ ];
+
+ $event->setForm($form);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function submitForm(BosCoreFormEvent $event): void {
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $input = $event->getFormState()->getUserInput()["bos_email"];
+ $event->setConfig("registry.service", $input["registry"]["service"]);
+ $event->setConfig("registry.template", $input["registry"]["template"]);
+ $event->setConfig("registry.enabled", $input["registry"]["enabled"] ?? 0);
+ $event->setConfig("registry.q_enabled", $input["registry"]["q_enabled"] ?? 0);
+
+ }
+ }
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
new file mode 100644
index 0000000000..056378340c
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
@@ -0,0 +1,150 @@
+ 'buildForm',
+ BosCoreFormEvent::CONFIG_FORM_SUBMIT => 'submitForm',
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getGroupID(): string {
+ return "sanitation";
+ }
+
+ /**
+ * Decodes the payload from a json string into an associative array.
+ *
+ * @param string $payload
+ *
+ * @return void
+ */
+ public static function fetchPayload(Request $request): array {
+
+ if ($request->getContentTypeFormat() != "json") {
+ return [];
+ }
+
+ $payload = $request->getContent();
+
+ if ($payload) {
+ try {
+ return json_decode($payload, TRUE);
+ }
+ catch (Exception $e) {
+ return [];
+ }
+ }
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function parseEmailFields(array &$payload, CobEmail &$email_object): void {
+
+ // Do the base email fields processing first.
+ parent::parseEmailFields($payload, $email_object);
+
+ // Set up the Postmark template.
+ $template_id = \Drupal::config("bos_email.settings")->get("sanitation.template");
+ $email_object->setField("TemplateID", $template_id);
+ $email_object->setField("Tag", $payload["type"]);
+
+ // Template expects "subject" not the default "Subject"
+ $email_object->setField("TemplateModel", ["subject" => $email_object->getField("Subject")]);
+
+ // is this to be scheduled?
+ if (!empty($payload["senddatetime"])) {
+ $email_object->setSendDate($payload["senddatetime"]);
+ }
+ else {
+ // Remove the field from the object.
+ $email_object->delField("senddatetime");
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function buildForm(BosCoreFormEvent $event): void {
+
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $form = $event->getForm();
+ $form["bos_email"]["sanitation"] = [
+ '#type' => 'fieldset',
+ '#title' => 'Sanitation Email Services',
+ '#markup' => 'Emails sent from Sanitation WebApp.',
+ '#collapsible' => FALSE,
+ '#weight' => 3,
+
+ "service" => [
+ "#type" => "select",
+ '#title' => t('Sanitation Email Service'),
+ '#description' => t('The Email Service which is currently being used.'),
+ "#options" => $form["service_options"],
+ '#default_value' => $event->getConfig('sanitation.service')
+ ],
+ "template" => [
+ "#type" => "textfield",
+ '#title' => t('Default Sanitation Email Template'),
+ '#description' => t('The ID for the template being used -leave blank if no template is required.'),
+ '#default_value' => $event->getConfig('sanitation.template')
+ ],
+ "enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation email service enabled'),
+ '#default_value' => $event->getConfig('sanitation.enabled'),
+ ],
+ "q_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation queue processing enabled'),
+ '#description' => t('When selected, emails which initially fail to send are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('sanitation.q_enabled'),
+ ],
+ "sched_enabled" => [
+ '#type' => 'checkbox',
+ '#title' => t('Sanitation scheduled email processing enabled'),
+ '#description' => t('When selected, scheduled emails are queued will be processed on each cron run.'),
+ '#default_value' => $event->getConfig('sanitation.sched_enabled'),
+ ],
+ ];
+
+ $event->setForm($form);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function submitForm(BosCoreFormEvent $event): void {
+ if ($event->getEventType() == "bos_email_config_settings") {
+ $input = $event->getFormState()->getUserInput()["bos_email"];
+ $event->setConfig("sanitation.service", $input["sanitation"]["service"]);
+ $event->setConfig("sanitation.template", $input["sanitation"]["template"]);
+ $event->setConfig("sanitation.enabled", $input["sanitation"]["enabled"] ?? 0);
+ $event->setConfig("sanitation.sched_enabled", $input["sanitation"]["sched_enabled"] ?? 0);
+ $event->setConfig("sanitation.q_enabled", $input["sanitation"]["q_enabled"] ?? 0);
+ }
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
index 15aa959db7..9f5eaf578e 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
@@ -5,14 +5,30 @@
use Boston;
use Drupal;
use Drupal\bos_email\EmailServiceInterface;
+use Drupal\Core\Render\Markup;
+use Drupal\Core\Site\Settings;
+use Drupal\bos_email\CobEmail;
/**
* Postmark class for API.
*/
class DrupalService implements EmailServiceInterface {
- const MESSAGE_SENT = 'Message sent.';
- const MESSAGE_QUEUED = 'Message queued.';
+ const DEFAULT_ENDPOINT = 'internal';
+ const TEMPLATE_ENDPOINT = 'internal';
+
+ /**
+ * @var array Retains the request
+ */
+ protected array $request;
+
+ /**
+ * An associative array created from the most recent CuRL transaction and
+ * which can be extended by any service extending this class.
+ *
+ * @var array
+ */
+ protected array $response;
public null|string $error;
@@ -23,6 +39,15 @@ public function id():string {
return "bos_email.DrupalService";
}
+ /**
+ * @inheritDoc
+ */
+ public function updateEmailObject(CobEmail &$email_object): void {
+ $email_object->setField("endpoint", self::DEFAULT_ENDPOINT);
+ $email_object->delField("TemplateID");
+ $email_object->delField("TemplateModel");
+ }
+
/**
* Send the email via Postmark.
*
@@ -32,6 +57,18 @@ public function id():string {
*/
public function sendEmail(array $item):bool {
+ // Check if we are sending out emails.
+ $config = Drupal::configFactory()->get("bos_email.settings");
+ if (!$config->get("enabled")) {
+ $this->error = "Emailing temporarily suspended for all emails";
+ Drupal::logger("bos_email:DrupalService")->error($this->error);
+ return FALSE;
+ }
+ elseif ($item["server"] && !$config->get(strtolower($item["server"]))["enabled"]) {
+ $this->error = "Emailing temporarily suspended for {$item["server"]} emails.";
+ Drupal::logger("bos_email:DrupalService")->error($this->error);
+ return FALSE;
+ }
/**
* @var \Drupal\Core\Mail\MailManager $mailManager
@@ -40,56 +77,133 @@ public function sendEmail(array $item):bool {
// Send the email.
$item["_error_message"] = "";
- $key = "{$this->service}.{$item["Tag"]}";
$mailManager = Drupal::service('plugin.manager.mail');
+ $sent = $mailManager->mail("bos_email", $item["server"] , $item["To"], "en", $item, $item["ReplyTo"], TRUE);
- $sent = $mailManager->mail("bos_email", $key , $item["To"], "en", $item, NULL, TRUE);
+ $this->response = [
+ "sent" => $sent ? "True" : "False",
+ ];
if (!$sent || !$sent["result"]) {
if (!empty($params["_error_message"])) {
+ $this->response["error"] = $params["_error_message"];
throw new \Exception($params["_error_message"]);
}
else {
+ $this->response["error"] = "Error sending email";
throw new \Exception("Error sending email.");
}
}
- $response_message = self::MESSAGE_SENT;
+ return TRUE;
}
catch (\Exception $e) {
- try {
- $this->addQueueItem($item);
- }
- catch (\Exception $ee) {
- Drupal::logger("bos_email:DrupalService")->info("Failed to queued mail item in {$item->getField("server")}");
- return [
- 'status' => 'error',
- 'response' => "Error sending message {$e->getMessage()}, then error queueing item {$ee->getMessage()}.",
- ];
- }
-
+ $this->error = $e->getMessage();
+ $this->response["error"] = $this->error;
if (Boston::is_local()) {
- Drupal::logger("bos_email:DrupalService")->info("Queued {$item->getField("server")}");
+ Drupal::logger("bos_email:DrupalService")
+ ->info("Queued {$item["server"]}");
+ return FALSE;
}
- $response_message = self::MESSAGE_QUEUED;
}
-
- return [
- 'status' => 'success',
- 'response' => $response_message,
- ];
-
}
/**
* @inheritDoc
*/
public function getVars(): array {
- // TODO: Implement getVars() method.
+
+ $postmark_env = [];
+ if (getenv('POSTMARK_SETTINGS')) {
+ $get_vars = explode(",", getenv('POSTMARK_SETTINGS'));
+ foreach ($get_vars as $item) {
+ $json = explode(":", $item);
+ if (!empty($json[0]) && !empty($json[1])) {
+ $postmark_env[$json[0]] = $json[1];
+ }
+ }
+ }
+ else {
+ $postmark_env = Settings::get('postmark_settings') ?? [];
+ }
+
+ return $postmark_env;
+ }
+
+ /**
+ * Format the body part of the email using twig templates.
+ * The idea here is to create Markup in the $message["body"] field.
+ *
+ * Called from bos_email.module bos_email_mail().
+ *
+ * @param array $params the Drupal mail params object
+ * @param array $message the Drupal mail message object
+ *
+ * @return void
+ */
+ public static function renderEmail(array &$params, array &$message):void {
+
+ // Map in the default values
+ $message["from"] = $params["From"];
+ $message["subject"] = $params["Subject"];
+ $message["reply-to"] = $params["ReplyTo"];
+ !empty($params["Cc"]) && $message['headers']["CC"] = $params["Cc"];
+ !empty($params["Bcc"]) && $message['headers']["BCC"] = $params["Bcc"];
+ $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
+
+ if (!empty($params["useHTML"]) && $params["useHTML"] == 1) {
+ // The $params["message"] field is already in HTML, so just use it.
+ // No twig templating required for the body.
+ if (is_string($params["message"])) {
+ $params["message"] = Markup::create($params["message"]);
+ }
+ $message["body"] = $params["message"];
+ return;
+ }
+
+ // Get the template name.
+ $path = Drupal::service('extension.list.module')
+ ->get('bos_email')
+ ->getPath();
+ $twig_service = Drupal::service('twig');
+
+ // Try to find the generic template
+ $template_name = "{$path}/templates/{$params["server"]}.body.html.twig";
+ if (file_exists($template_name)) {
+ $rendered_template = $twig_service->render($template_name, $params);
+ $params['message'] = Markup::create($rendered_template);
+ $message["body"] = $params["message"];
+ return;
+ }
+
+ // Create a variant field which the template can use to determine body
+ // copy/format.
+ if (!empty($params["Tag"])) {
+ $params['variant'] = "{$params["server"]}.body.{$params["Tag"]}";
+ // Try to find the processor-specific template
+ $template_name = "{$path}/templates/{$params["variant"]}.html.twig";
+ if (file_exists($template_name)) {
+ $rendered_template = $twig_service->render($template_name, $params);
+ $params['message'] = Markup::create($rendered_template);
+ $message["body"] = $params["message"];
+ return;
+ }
+ }
+
+ // Use a generic body template.
+ $template_name = "{$path}/templates/default.body.html.twig";
+ $rendered_template = $twig_service->render($template_name, $params);
+ $params['message'] = Markup::create($rendered_template);
+ $message["body"] = $params["message"];
+
+ }
+
+ public function response(): array {
+ return $this->response ?? [];
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
index 6da7f84405..064f416b4d 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
@@ -3,6 +3,7 @@
namespace Drupal\bos_email\Services;
use Drupal\bos_core\Controllers\Curl\BosCurlControllerBase;
+use Drupal\bos_email\CobEmail;
use Drupal\bos_email\EmailServiceInterface;
use Drupal\Core\Site\Settings;
use Exception;
@@ -12,10 +13,13 @@
*/
class PostmarkService extends BosCurlControllerBase implements EmailServiceInterface {
+ const DEFAULT_ENDPOINT = 'https://api.postmarkapp.com/email';
+ const TEMPLATE_ENDPOINT = "https://api.postmarkapp.com/email/withTemplate";
+
// Make this protected var from BosCurlControllerBase public
public null|string $error;
- public array $response;
+// public array $response;
/**
* @inheritDoc
@@ -46,6 +50,43 @@ public function getVars(): array {
return $postmark_env;
}
+ /**
+ * @inheritDoc
+ */
+ public function updateEmailObject(CobEmail &$email_object): void {
+ if (empty($email_object->getField("TemplateID"))) {
+ $email_object->setField("endpoint", $this::DEFAULT_ENDPOINT);
+ $email_object->delField("TemplateID");
+ $email_object->delField("TemplateModel");
+ }
+ else {
+ if (!is_numeric($email_object->getField("TemplateID"))) {
+ // For PostMark Templates, the tmeplate can be referred to using a
+ // numeric ID or an alias.
+ // Looks like an alias is being used, so update the $email_object to
+ // send the alias not the ID.
+ $email_object->addField("TemplateAlias", $email_object::FIELD_STRING, $email_object->getField("TemplateID"));
+ $email_object->delField("TemplateID");
+ }
+ $email_object->setField("endpoint", $this::TEMPLATE_ENDPOINT);
+
+ // If the EmailProcessor (in ::parseEmailFields()) has set the
+ // TemplateModel (a set of arguments to pass to the template) then merge
+ // these defaults in, but retain the existing.
+ $model = [
+ "Subject" => $email_object->getField("Subject"),
+ "TextBody" => ($email_object->getField("TextBody") ?? ($email_object->getField("HtmlBody") ?? ($email_object->getField("message") ?? ""))),
+ "ReplyTo" => $email_object->getField("ReplyTo"),
+ ];
+ $model = array_merge($model, $email_object->getField("TemplateModel"));
+ $email_object->setField("TemplateModel", $model);
+ // Cleanup
+ $email_object->delField("ReplyTo");
+ $email_object->delField("Subject");
+ $email_object->delField("TextBody");
+ }
+ }
+
/**
* Send email to Postmark.
*/
@@ -54,7 +95,7 @@ public function sendEmail(array $item):bool {
// Check if we are sending out emails.
$config = \Drupal::configFactory()->get("bos_email.settings");
if (!$config->get("enabled")) {
- $this->error = "Emailing temporarily suspended for all PostMark emails";
+ $this->error = "Emailing temporarily suspended for all emails";
\Drupal::logger("bos_email:PostmarkService")->error($this->error);
return FALSE;
}
@@ -91,12 +132,13 @@ public function sendEmail(array $item):bool {
throw new \Exception("Posting Error {$this->response["http_code"]}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
}
- if (strtolower($response["ErrorCode"]) != "0") {
+ if (!$response && !empty($this->response["response_raw"])) {
+ $response = json_decode($this->response["response_raw"], TRUE);
+ $this->error = "Error code ({$response["ErrorCode"]}) returned from Postmark - {$response["Message"]}";
$headers = json_encode($headers);
$item = json_encode($item);
$response = json_encode($this->response);
- $this->error = "Error code returned from Postmark - {$response["Message"]}";
- throw new \Exception("Return Error Code: {$response['ErrorCode']}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
+ throw new \Exception("Return Error Code: {$this->response["http_code"]}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
}
return TRUE;
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
deleted file mode 100644
index 6f77ddefc2..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Contactform.php
+++ /dev/null
@@ -1,197 +0,0 @@
-setField("Tag", "Contact Form");
-
- if (isset($emailFields["endpoint"])) {
- $cobdata->addField("endpoint", "string", $emailFields["endpoint"]);
- }
- else {
- $cobdata->addField("endpoint", "string", EmailController::POSTMARK_DEFAULT_ENDPOINT);
- }
-
- self::templatePlainText($emailFields);
- if (!empty($emailFields["useHtml"])) {
- self::templateHtmlText($emailFields);
- }
-
- // Create a hash of the original poster's email
- $hashemail = $cobdata::encodeFakeEmail($emailFields["from_address"], self::OUTBOUND_DOMAIN );
- $cobdata->setField("Metadata", [
- "opmail" => $cobdata::hashText($emailFields["from_address"], $cobdata::ENCODE)
- ]);
-
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", "Boston.gov Contact Form <{$hashemail}>");
- $cobdata->setField("ReplyTo", $emailFields["from_address"]);
- isset($emailFields["name"]) && $cobdata->setField("ReplyTo", "{$emailFields["name"]}<{$emailFields["from_address"]}>");
- !empty($emailFields['cc']) && $cobdata->setField("Cc", $emailFields['cc']);
- !empty($emailFields['bcc']) && $cobdata->setField("Bcc", $emailFields['bcc']);
- $cobdata->setField("Subject", $emailFields["subject"]);
- !empty($emailFields['headers']) && $cobdata->setField("Headers", $emailFields['headers']);
- !empty($emailFields['tag']) && $cobdata->setField("Tag", $emailFields['tag']);
-
- if (empty($emailFields["TemplateID"]) && empty($emailFields["template_id"])) {
- // Remove redundant fields
- $cobdata->delField("TemplateModel");
- $cobdata->delField("TemplateID");
- }
- else {
- // An email template is to be used.
- $cobdata->setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
- $cobdata->delField("TextBody");
- $cobdata->delField("Subject");
- $cobdata->delField("HtmlBody");
- }
-
- }
-
- /**
- * @inheritDoc
- */
- public static function templatePlainText(&$emailFields): void {
-
- $cobdata = &$emailFields["email_object"];
- $msg = strip_tags($emailFields["message"]);
-
- if (empty($emailFields["TemplateID"]) && empty($emailFields["template_id"])) {
- $text = "-- REPLY ABOVE THIS LINE -- \n\n";
- $text .= "{$msg}\n\n";
- $text .= "{$emailFields["phone"]}\n\n";
- $text .= "-------------------------------- \n";
- $text .= "This message was sent using the contact form on Boston.gov.";
- $text .= " It was sent by {$emailFields["name"]} from {$emailFields["from_address"]} and {$emailFields["phone"]}.";
- $text .= " It was sent from {$emailFields["url"]}.\n\n";
- $text .= "-------------------------------- \n";
- $cobdata->setField("TextBody", $text);
- }
- else {
- // we are using a template
- $cobdata->delField("TextBody");
- $cobdata->setField("TemplateID", $emailFields['TemplateID']);
- $cobdata->setField("TemplateModel", [
- "subject" => $emailFields["subject"],
- "TextBody" => $msg,
- "ReplyTo" => $emailFields["from_address"],
- ]);
- $emailFields["useHtml"] = 0;
- }
-
- }
-
- /**
- * @inheritDoc
- */
- public static function templateHtmlText(&$emailFields): void {
-
- if (empty($emailFields["TemplateID"]) && empty($emailFields["template_id"])) {
-
- $cobdata = &$emailFields["email_object"];
-
- $msg = Html::escape(Xss::filter($emailFields["message"]));
- $msg = str_replace("\n", "
", $msg);
-
- $html = "
----- REPLY ABOVE THIS LINE -----
";
- $html .= "{$msg}
";
- $html .= "
";
- $html .= "{$emailFields["phone"]}";
- $html .= "
";
- $html .= " | ";
- $html .= "This message was sent using the contact form on Boston.gov. ";
- $html .= " It was sent by {$emailFields["name"]} from {$emailFields["from_address"]} and {$emailFields["phone"]}. ";
- $html .= " It was sent from {$emailFields["url"]}. | ";
- $html .= "
";
- $html .= "
";
-
- $cobdata->setField("HtmlBody", $html);
-
- }
-
- }
-
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void {
-
-// if ($emailFields["endpoint"]->getField("server") == "contactform"
-// && str_contains($emailFields["OriginalRecipient"], "@web-inbound.boston.gov")) {
-// $server = EmailController::AUTORESPONDER_SERVERNAME;
-// }
-
- // Find the original recipient
-
- // Create the email.
- /**
- * @var $cobdata CobEmail
- */
- $cobdata = &$emailFields["email_object"];
- $original_recipient = $cobdata::decodeFakeEmail($emailFields["OriginalRecipient"]);
- $cobdata->setField("To", $original_recipient);
- $cobdata->setField("From", "contactform@boston.gov");
- $cobdata->setField("Subject", $emailFields["Subject"]);
- $cobdata->setField("HtmlBody", $emailFields["HtmlBody"]);
- $cobdata->setField("TextBody", $emailFields["TextBody"]);
- $cobdata->setField("endpoint", EmailController::POSTMARK_DEFAULT_ENDPOINT);
- // Select Headers
- $cobdata->processHeaders($emailFields["Headers"]);
-
- // Remove redundant fields
- $cobdata->delField("TemplateModel");
- $cobdata->delField("TemplateID");
-
- }
-
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- return "contact";
- }
-
- /**
- * @inheritDoc
- */
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
- }
-
- /**
- * @inheritDoc
- */
- public static function getGroupID(): string {
- return "contactform";
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
deleted file mode 100644
index 04b7875909..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Registry.php
+++ /dev/null
@@ -1,100 +0,0 @@
-setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
-
- // Set up the Postmark template.
- $cobdata->setField("TemplateID", $emailFields["template_id"]);
- $cobdata->setField("TemplateModel", [
- "subject" => $emailFields["subject"],
- "TextBody" => $emailFields["message"],
- "ReplyTo" => $emailFields["from_address"]
- ]);
- $cobdata->delField("HtmlBody");
- $cobdata->delField("TextBody");
- $cobdata->delField("Subject");
-
- // Set general email fields.
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["from_address"]);
- isset($emailFields["name"]) && $cobdata->setField("ReplyTo", "{$emailFields["name"]}<{$emailFields["from_address"]}>");
- !empty($emailFields['cc']) && $cobdata->setField("Cc", $emailFields['cc']);
- !empty($emailFields['bcc']) && $cobdata->setField("Bcc", $emailFields['bcc']);
-
- // Create a relevant tag.
- if (str_contains($emailFields["subject"], "Birth")) {
- $cobdata->setField("Tag", "Birth Certificate");
- }
- elseif (str_contains($emailFields["subject"], "Intention")) {
- $cobdata->setField("Tag", "Marriage Intention");
- }
- elseif (str_contains($emailFields["subject"], "Death")) {
- $cobdata->setField("Tag", "Death Certificate");
- }
-
- }
-
- /**
- * @inheritDoc
- */
- public static function templatePlainText(&$emailFields): void {
- }
-
- /**
- * @inheritDoc
- */
- public static function templateHtmlText(&$emailFields): void {
- }
-
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- return "";
- }
-
- /**
- * @inheritDoc
- */
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
- }
-
-
- /**
- * @inheritDoc
- */
- public static function getGroupID(): string {
- return "registry";
- }
-
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void {
- // TODO: Implement incoming() method.
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
deleted file mode 100644
index 1c597edb74..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/Sanitation.php
+++ /dev/null
@@ -1,105 +0,0 @@
-setField("endpoint", EmailController::POSTMARK_TEMPLATE_ENDPOINT);
-
- // Set up the Postmark template.
- $template_id = \Drupal::config("bos_email.settings")->get("sanitation.template");
- $cobdata->setField("TemplateID", $template_id);
- $cobdata->setField("TemplateModel", [
- "subject" => $emailFields["subject"],
- "TextBody" => $emailFields["message"],
- "ReplyTo" => $emailFields["from_address"]
- ]);
- $cobdata->delField("HtmlBody");
- $cobdata->delField("TextBody");
- $cobdata->delField("Subject");
-
- // Set general email fields.
- $cobdata->setField("To", $emailFields["to_address"]);
- $cobdata->setField("From", $emailFields["from_address"]);
- $cobdata->setField("ReplyTo", $emailFields["from_address"]);
-
- $cobdata->setField("Tag", $emailFields["type"]);
-
- // is this to be scheduled?
- if (!empty($emailFields["senddatetime"])) {
- try {
- $senddatetime = strtotime($emailFields["senddatetime"]);
- $cobdata->setField("senddatetime", $senddatetime);
- }
- catch (Exception $e) {
- $cobdata->delField("senddatetime");
- }
- }
- else {
- $cobdata->delField("senddatetime");
- }
-
- }
-
- /**
- * @inheritDoc
- */
- public static function templatePlainText(&$emailFields): void {
- // Only use templates ATM.
- }
-
- /**
- * @inheritDoc
- */
- public static function templateHtmlText(&$emailFields): void {
- // Only use templates ATM.
- }
-
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- return "";
- }
-
- /**
- * @inheritDoc
- */
- public static function getEmailService(): EmailServiceInterface {
- $config = \Drupal::service("config.factory")->get("bos_email.settings");
- $email_service = $config->get(self::getGroupID() . ".service");
- $email_service = "Drupal\\bos_email\\Services\\{$email_service}";
- return new $email_service;
- }
-
- /**
- * @inheritDoc
- */
- public static function getGroupID(): string {
- return "sanitation";
- }
-
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void {
- // Not Used
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
deleted file mode 100644
index 99a8645ea5..0000000000
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Templates/TestDrupalmail.php
+++ /dev/null
@@ -1,68 +0,0 @@
-setField("To", $emailFields["To"]);
- $data->setField("From", "Drupal {$data->getField("endpoint")} Test ");
- $data->setField("Subject", $emailFields["Subject"]);
- $data->setField("TextBody", $emailFields["TextBody"]);
-
- if ($emailFields["htmlBody"]) {
- $data->setField("HtmlBody", $emailFields["HtmlBody"]);
- }
- else{
- $data->delField("HtmlBody");
- }
-
- $data->delField("TemplateID");
- $data->delField("TemplateModel");
- }
-
- /**
- * @inheritDoc
- */
- public static function templatePlainText(&$emailFields): void { }
-
- /**
- * @inheritDoc
- */
- public static function templateHtmlText(&$emailFields): void { }
-
- /**
- * @inheritDoc
- */
- public static function formatInboundEmail(array &$emailFields): void { }
-
- /**
- * @inheritDoc
- */
- public static function getHoneypotField(): string {
- return "";
- }
-
- /**
- * @inheritDoc
- */
- public static function getGroupID(): string {
- return "drupal_mail";
- }
-
-}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/default.body.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/default.body.html.twig
new file mode 100644
index 0000000000..11ae84343b
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/default.body.html.twig
@@ -0,0 +1,28 @@
+{#
+This is a standardized template copied from code used by Postmark.
+The template receives the following useful fields:
+ To: The email recipient
+ ReplyTo: The reply-to recipient
+ From: The email sender
+ server: The email-caller service
+ Tag: The email-caller sub-category
+ Subject: The email subject
+ TextBody: The raw text for the body
+ server: the Email Processor used
+ service: the Email Service used (as a class)
+ _error_message: Any error message.
+ variant: The variant we are currently processing.
+#}
+
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_footer.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_footer.html.twig
new file mode 100644
index 0000000000..474f39e379
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_footer.html.twig
@@ -0,0 +1,11 @@
+
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_head.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_head.html.twig
new file mode 100644
index 0000000000..9f50d0fa8f
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_head.html.twig
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_masthead.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_masthead.html.twig
new file mode 100644
index 0000000000..be03f8c67c
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/includes/standard_masthead.html.twig
@@ -0,0 +1,8 @@
+
+
+
+
+ |
+
+
+
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/mimemail-message--bos-email.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/mimemail-message--bos-email.html.twig
new file mode 100644
index 0000000000..ff6b026653
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/mimemail-message--bos-email.html.twig
@@ -0,0 +1,72 @@
+{#
+/**
+ * @file
+ * Default template to format a HTML email using the Mime Mail module.
+ *
+ * Copy this file in your default theme folder to create a custom themed email.
+ * If you modify this template you MUST be sure to keep the html, body, and
+ * header tags. This template should produce a fully-formed HTML document.
+ * Failure to include these will result in a malformed email and possibly
+ * errors shown to the user when sending email.
+ *
+ * To override this template for all emails sent by a given module,
+ * rename this template to mimemail-messages--[module].html.twig.
+ *
+ * To override this template for a specific email sent by a given module,
+ * rename this template to mimemail-messages--[module]--[key].html.twig.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the body element of the message.
+ * - key: The message identifier.
+ * - module: The machine name of the sending module.
+ * - css: Internal style sheets.
+ * - recipient: The recipient of the message.
+ * - subject: The message subject.
+ * - body: The message body.
+ *
+ * @see template_preprocess_mimemail_message()
+ *
+ * @ingroup themeable
+ */
+#}
+
+{% set classes = module ? (key ? module ~ '-' ~ key) %}
+
+
+
+
+
+
+ {% include '@bos_email/includes/standard_head.html.twig' %}
+ {% if css %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% include '@bos_email/includes/standard_masthead.html.twig' %}
+ |
+
+
+ {{ body }}
+ |
+
+
+
+
+
+
+
+
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/templates/sanitation.body.html.twig b/docroot/modules/custom/bos_components/modules/bos_email/templates/sanitation.body.html.twig
new file mode 100644
index 0000000000..388f16cbab
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_email/templates/sanitation.body.html.twig
@@ -0,0 +1,29 @@
+{#
+This is a standardized template copied from code used by Postmark.
+The template receives the following useful fields:
+ To: The email recipient
+ ReplyTo: The reply-to recipient
+ From: The email sender
+ server: The email-caller service
+ Tag: The email-caller sub-category
+ Subject: The email subject
+ TextBody: The raw text for the body
+ server: the Email Processor used
+ service: the Email Service used (as a class)
+ _error_message: Any error message.
+ variant: The variant we are currently processing.
+#}
+
diff --git a/docroot/modules/custom/bos_core/src/Event/BosCoreFormEvent.php b/docroot/modules/custom/bos_core/src/Event/BosCoreFormEvent.php
new file mode 100644
index 0000000000..16864e0906
--- /dev/null
+++ b/docroot/modules/custom/bos_core/src/Event/BosCoreFormEvent.php
@@ -0,0 +1,94 @@
+form = $form;
+ $this->form_state = $form_state;
+ $this->eventType = $event_type;
+ $this->config = $config;
+ }
+
+ /**
+ * Method to get the form array from the event.
+ */
+ public function getForm(): array {
+ return $this->form;
+ }
+
+ public function setForm(array $form): void {
+ $this->form = $form;
+ }
+
+ /**
+ * Method to get the form_state from the event.
+ */
+ public function getFormState(): FormStateInterface {
+ return $this->form_state;
+ }
+ /**
+ * Method to get the event type.
+ */
+ public function getEventType() {
+ return $this->eventType;
+ }
+
+ public function getConfig(?string $key = NULL): string {
+ if (empty($key)) {
+ return $this->config->get();
+ }
+ return $this->config->get($key) ?? FALSE;
+ }
+
+ public function setConfig(string $key, string $value): void {
+ $this->config->set($key, $value)->save();
+ }
+
+}
From afea65177da119c1a239fc9cca38e42812c1e8ef Mon Sep 17 00:00:00 2001
From: David Upton
Date: Wed, 24 Apr 2024 18:44:34 -0400
Subject: [PATCH 06/48] Update http-client.env.json
---
.../bos_components/modules/bos_email/http-client.env.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
index 7c1239a994..545086bde2 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
+++ b/docroot/modules/custom/bos_components/modules/bos_email/http-client.env.json
@@ -1,7 +1,7 @@
{
"LOCAL for testing": {
"url": "https://boston.lndo.site",
- "bos_email_bearer_token": "c43517a240ee61898c00600eaa775aa0d0e639322c3f275b780f66062f69",
+ "bos_email_bearer_token": "",
"contact_form_session_token": "",
"registry_template_id": "31135208",
"email_test_recipient": "david.upton@boston.gov",
@@ -9,7 +9,7 @@
},
"CI": {
"url": "https://d8-ci.boston.gov",
- "bos_email_bearer_token": "c43517a240ee61898c00600eaa775aa0d0e639322c3f275b780f66062f69",
+ "bos_email_bearer_token": "",
"contact_form_session_token": "",
"registry_template_id": "31135208",
"email_test_recipient": "david.upton@boston.gov",
From baf6e239fc6277352dce9b152e0ec6f8502e7d9f Mon Sep 17 00:00:00 2001
From: David Upton
Date: Wed, 24 Apr 2024 18:56:36 -0400
Subject: [PATCH 07/48] DIG-4317 Allows HTML formatting in Postmark templates
---
.../custom/bos_components/modules/bos_email/bos_email.http | 2 +-
.../bos_components/modules/bos_email/src/CobEmail.php | 2 ++
.../modules/bos_email/src/EmailProcessorInterface.php | 2 +-
.../src/Plugin/EmailProcessor/EmailProcessorBase.php | 3 ++-
.../bos_email/src/Plugin/EmailProcessor/Registry.php | 3 +++
.../bos_email/src/Plugin/EmailProcessor/Sanitation.php | 6 ++++--
.../modules/bos_email/src/Services/PostmarkService.php | 4 ++--
7 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
index cd43f10093..5c7f53c1a6 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -70,7 +70,7 @@ Force-Service: {{ force_email_service }}
"to_address": "{{email_test_recipient}}",
"from_address": "Sanitation ",
"subject": "Sanitation Confirmation",
- "message": "We are pleased to confirm your pickup",
+ "message": "We are pleased to confirm your pickup
",
"type": "confirmation"
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
index 7a51a09df2..7dba4f999d 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/CobEmail.php
@@ -23,6 +23,7 @@ class CobEmail {
"TemplateModel" => [
"subject" => "",
"TextBody" => "",
+ "HtmlBody" => "",
"ReplyTo" => "",
],
"endpoint" => "",
@@ -48,6 +49,7 @@ class CobEmail {
"TemplateModel" => [
"subject" => "string",
"TextBody" => "string",
+ "HtmlBody" => "html",
"ReplyTo" => "email",
],
"service" => "string",
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
index 0dcc94cbe5..64a310c988 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
@@ -8,7 +8,7 @@
interface EmailProcessorInterface {
/**
- * Custom mpaaing of the data in $payload into the structured array $emailFields.
+ * Custom mapping of the data in $payload into the structured array $emailFields.
* We can create new fields in the $emailFields using ->addField but this
* should be an unusal circumstance. More commnly, when using a Template, we
* can set additional template arguments/parameters from the payload in this
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
index baeddebe85..d2453fd158 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/EmailProcessorBase.php
@@ -85,7 +85,8 @@ public static function parseEmailFields(array &$payload, CobEmail &$email_object
$email_object->setField("Tag", ($payload['tag'] ?? ""));
$email_object->setField("To", ($payload["to_address"] ?? ($payload["to"] ?? ($payload["recipient"] ?? ""))));
$email_object->setField("From", ($payload["modified_from_address"] ?? ($payload["from_address"] ?? "")));
- $email_object->setField("TextBody", ($payload["message"] ?? ($payload["body"] ?? "")));
+ $email_object->setField("TextBody", ($payload["TextBody"] ?: ($payload["message"] ?: ($payload["body"] ?: ""))));
+ $email_object->setField("HtmlBody", ($payload["HtmlBody"] ?: ($payload["message"] ?: ($payload["body"] ?: ""))));
$email_object->setField("ReplyTo", ($payload["reply_to"] ?? ($payload["from_address"] ?? "")));
!empty($payload['subject']) && $email_object->setField("Subject", $payload["subject"]);
!empty($payload['cc']) && $email_object->setField("Cc", $payload['cc']);
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
index 53fc66cade..46878f41fe 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
@@ -33,6 +33,9 @@ public static function parseEmailFields(array &$payload, CobEmail &$email_object
$email_object->setField("TemplateID", $payload["template_id"]);
isset($emailFields["name"]) && $email_object->setField("ReplyTo", "{$payload["name"]}<{$emailFields["from_address"]}>");
+ $email_object->setField("TemplateModel", [
+ "TextBody" => ($email_object->getField("TextBody") ?: ($email_object->getField("HtmlBody") ?: ($email_object->getField("message") ?: ""))),
+ ]);
// Create a relevant tag.
if (str_contains($payload["subject"], "Birth")) {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
index 056378340c..c4e81b44db 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
@@ -69,8 +69,10 @@ public static function parseEmailFields(array &$payload, CobEmail &$email_object
$email_object->setField("TemplateID", $template_id);
$email_object->setField("Tag", $payload["type"]);
- // Template expects "subject" not the default "Subject"
- $email_object->setField("TemplateModel", ["subject" => $email_object->getField("Subject")]);
+ $email_object->setField("TemplateModel", [
+ "TextBody" => ($email_object->getField("TextBody") ?: ($email_object->getField("HtmlBody") ?: ($email_object->getField("message") ?: ""))),
+ "HtmlBody" => ($email_object->getField("HtmlBody") ?: ($email_object->getField("TextBody") ?: ($email_object->getField("message") ?: ""))),
+ ]);
// is this to be scheduled?
if (!empty($payload["senddatetime"])) {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
index 064f416b4d..7bef0a848e 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
@@ -74,8 +74,7 @@ public function updateEmailObject(CobEmail &$email_object): void {
// TemplateModel (a set of arguments to pass to the template) then merge
// these defaults in, but retain the existing.
$model = [
- "Subject" => $email_object->getField("Subject"),
- "TextBody" => ($email_object->getField("TextBody") ?? ($email_object->getField("HtmlBody") ?? ($email_object->getField("message") ?? ""))),
+ "subject" => $email_object->getField("Subject"),
"ReplyTo" => $email_object->getField("ReplyTo"),
];
$model = array_merge($model, $email_object->getField("TemplateModel"));
@@ -84,6 +83,7 @@ public function updateEmailObject(CobEmail &$email_object): void {
$email_object->delField("ReplyTo");
$email_object->delField("Subject");
$email_object->delField("TextBody");
+ $email_object->delField("HtmlBody");
}
}
From d4c990d334b9b4fc7d149e7405b3c46d8b0ea4b5 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Thu, 25 Apr 2024 09:07:53 -0400
Subject: [PATCH 08/48] DIG-4317 Extra testing
---
.../modules/bos_email/bos_email.http | 33 +------------------
1 file changed, 1 insertion(+), 32 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
index 5c7f53c1a6..6d60ed3f6a 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -64,7 +64,7 @@ POST {{url}}/rest/email/sanitation
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Type: application/json
Authorization: Token {{bos_email_bearer_token}}
-Force-Service: {{ force_email_service }}
+#Force-Service: {{ force_email_service }}
{
"to_address": "{{email_test_recipient}}",
@@ -1011,34 +1011,3 @@ Force-Service: {{ force_email_service }}
client.assert(response.body.response.indexOf("email is not valid") != false, "Response does not report a bad email address");
});
%}
-
-###
-# group: bos_email / fail-test
-# @name Bad scheduled date
-POST {{url}}/rest/email/sanitation
-Cookie: XDEBUG_SESSION=PHPSTORM
-Content-Type: application/json
-Authorization: Token {{bos_email_bearer_token}}
-Force-Service: {{ force_email_service }}
-
-{
- "to_address": "{{email_test_recipient}}",
- "from_address": "Sanitation ",
- "subject": "Sanitation Confirmation",
- "message": "We are pleased to confirm your pickup",
- "type": "confirmation",
- "senddatetime": "-1 days"
-}
-
-> {%
- client.test("Status code is 400", function () {
- client.assert(response.status == 400, "Response HTTP Code is not 400");
- });
- client.test("Response is json", function () {
- var type = response.contentType.mimeType;
- client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'");
- });
- client.test("Expected Response - Scheduled date is in the past", function () {
- client.assert(response.body.response == "Scheduled date is in the past.", "Response status field is not 'Scheduled date is in the past'");
- });
-%}
From 4ec3c8fd2496902d8555832e69bbf09d7cabbb5e Mon Sep 17 00:00:00 2001
From: David Upton
Date: Thu, 25 Apr 2024 23:51:34 -0400
Subject: [PATCH 09/48] DIG-4317 Refactoring and commenting
---
.../modules/bos_email/bos_email.http | 4 -
.../src/Controller/EmailController.php | 98 ++++++++++++-------
.../bos_email/src/EmailProcessorInterface.php | 2 +-
.../bos_email/src/EmailServiceInterface.php | 5 +-
.../src/Plugin/EmailProcessor/Commissions.php | 2 +-
.../src/Plugin/EmailProcessor/Contactform.php | 2 +-
.../Plugin/EmailProcessor/DefaultEmail.php | 2 +-
.../MetrolistInitiationForm.php | 2 +-
.../MetrolistListingConfirmation.php | 4 +-
.../MetrolistListingNotification.php | 2 +-
.../src/Plugin/EmailProcessor/Registry.php | 2 +-
.../src/Plugin/EmailProcessor/Sanitation.php | 4 +-
.../QueueWorker/ContactformProcessItems.php | 49 ++++++----
.../QueueWorker/ScheduledEmailProcessor.php | 83 ++++++++++------
.../bos_email/src/Services/DrupalService.php | 57 ++++-------
.../src/Services/PostmarkService.php | 26 ++---
16 files changed, 187 insertions(+), 157 deletions(-)
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
index 6d60ed3f6a..785a3c2b5f 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
+++ b/docroot/modules/custom/bos_components/modules/bos_email/bos_email.http
@@ -816,10 +816,6 @@ Force-Service: {{ force_email_service }}
client.assert(response.body.status == "error", "Response status field is not 'error'");
client.assert(response.body.response == "Could not evaluate scheduled date.", "Response message is not 'Could not evaluate scheduled date.'");
});
- client.test("Response contains email id", function () {
- client.assert(typeof response.body.id !== "undefined" && response.body.id != "", "Response does not contain an ID field");
- client.global.set("sanitation_email_id", response.body.id);
- });
%}
###
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
index b7b8ae9512..2dfd548bae 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Controller/EmailController.php
@@ -5,6 +5,8 @@
use Boston;
use Drupal;
use Drupal\bos_email\CobEmail;
+use Drupal\bos_email\EmailProcessorInterface;
+use Drupal\bos_email\EmailServiceInterface;
use Drupal\bos_email\Services\TokenOps;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Controller\ControllerBase;
@@ -14,7 +16,7 @@
use Drupal\Core\Config\ImmutableConfig;
/**
- * Postmark class for API.
+ * Email Controller class for API.
*
* This token is best used when a client-side javascript code is used to call
* the bos_email APIs.
@@ -49,7 +51,8 @@ class EmailController extends ControllerBase {
public ImmutableConfig $config;
/**
- * Server hosted / mapped to Postmark.
+ * Records the group for this request.
+ * A group is a general classification for emails which use the same processor.
*
* @var string
*/
@@ -60,15 +63,32 @@ class EmailController extends ControllerBase {
*/
public $debug;
+ /**
+ * @var EmailServiceInterface $email_service The Email sending service being used.
+ *
+ */
private $email_service;
+ /**
+ * Errors at the class level.
+ *
+ * @var string $error
+ */
private string $error = "";
- /** @var \Drupal\bos_email\EmailProcessorInterface */
+ /**
+ * @var EmailProcessorInterface The email processor being used
+ */
private $email_processor;
+ /**
+ * @var string $honeypot The honeypot firld for this request.
+ */
private string $honeypot;
+ /**
+ * @var bool $authenticated Flag to indicate if this request has been authenticated yet.
+ */
private bool $authenticated = FALSE;
/**
@@ -701,8 +721,10 @@ public function callback(string $service, string $stream) {
/**
* Creates the email object which will be used by the email service.
*
- * @param array $emailFields
- * The array containing Postmark API needed fieds.
+ * @param \Drupal\bos_email\CobEmail $email_object
+ *
+ * @return bool
+ * @throws \Exception
*/
private function verifyEmailObject(CobEmail $email_object) {
@@ -743,42 +765,48 @@ private function verifyEmailObject(CobEmail $email_object) {
*/
private function sendEmail(CobEmail $email) {
- /**
- * @var $mailobj CobEmail
- */
-
- // Extract the email object, and validate.
+ // Validate the email object and extract the email fields array.
try {
$mailobj = $email->data();
}
catch (\Exception $e) {}
-
if ($email->hasValidationErrors()) {
return [
'status' => 'failed',
'response' => implode(":", $email->getValidationErrors()),
];
-
}
- // Send the email.
- try {
- $sent = $this->email_service->sendEmail($mailobj);
+ // Check if we are able to send the mail.
+ $sent = FALSE;
+ if (!$this->config->get("enabled")) {
+ // The whole email sending thing is shut down. Will still queue the
+ // emails though.
+ $this->error = "Emailing temporarily suspended for all emails";
+ \Drupal::logger("bos_email:EmailController")->error($this->error);
}
- catch (\Exception $e) {
- Drupal::logger("bos_email:PostmarkService")
- ->error($this->email_service->error);
- $sent = FALSE;
+ elseif ($mailobj["server"] && !$this->config->get(strtolower($mailobj["server"]))["enabled"]) {
+ // Just this EmailProcessor is shut down. Will still queue the emails.
+ $this->error = "Emailing temporarily suspended for {$mailobj["server"]} emails.";
+ \Drupal::logger("bos_email:EmailController")->error($this->error);
+ }
+ else {
+ // Send the email.
+ try {
+ $this->email_service->sendEmail($mailobj);
+ $sent = TRUE;
+ }
+ catch (\Exception $e) {
+ EmailController::alertHandler($mailobj, $this->email_service->response(), $this->email_service->response()["http_code"]);
+ $this->error = $e->getMessage();
+ Drupal::logger("bos_email:EmailController")->error($this->error);
+ }
}
if (!$sent) {
- EmailController::alertHandler($mailobj, $this->email_service->response(), $this->email_service->response()["http_code"]);
- Drupal::logger("bos_email:PostmarkService")
- ->error($this->email_service->error);
-
- // Add email data to queue because of Postmark failure.
- $mailobj["send_error"] = $this->email_service->error;
+ // Add email data to queue because of sending failure.
+ $mailobj["send_error"] = $this->error;
$this->addQueueItem($mailobj);
Drupal::logger("bos_email:EmailController")
@@ -788,7 +816,7 @@ private function sendEmail(CobEmail $email) {
}
else {
- // Message was sent successfully to sender via Postmark.
+ // Message was sent successfully to recipient.
$response_message = self::MESSAGE_SENT;
}
@@ -851,7 +879,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
$recipient = $config->get("alerts.recipient") ?? FALSE;
if ($recipient) {
- // Catch suppressed emails at PostMark
+ // Catch suppressed emails (at PostMark)
if ($config->get("hardbounce.hardbounce")
&& isset($response["ErrorCode"])
&& strtolower($response["ErrorCode"]) == "406") {
@@ -861,7 +889,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
$recipient = $config->get("hardbounce.recipient");
}
if (!$mailManager->mail("bos_email", 'hardbounce', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ Drupal::logger("bos_email:EmailController")->warning(t("Email sending from Drupal has failed."));
}
}
@@ -872,7 +900,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
$item["token_type"] = "Authentication Token";
$mailManager = Drupal::service('plugin.manager.mail');
if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ Drupal::logger("bos_email:EmailController")->warning(t("Email sending from Drupal has failed."));
}
}
@@ -883,18 +911,18 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
$item["token_type"] = "Session Token";
$mailManager = Drupal::service('plugin.manager.mail');
if (!$mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ Drupal::logger("bos_email:EmailController")->warning(t("Email sending from Drupal has failed."));
}
}
- // When the token needed by PostMark is invalid
+ // When the Auth token is incorrect
elseif ($config->get("alerts.token")
&& $error
&& str_contains($error, "Cannot find token")) {
$item["token_type"] = "PostMark Server API Token";
$mailManager = Drupal::service('plugin.manager.mail');
if ($mailManager->mail("bos_email", 'alerts.token', $recipient, "en", $item, NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ Drupal::logger("bos_email:EmailController")->warning(t("Email sending from Drupal has failed."));
}
}
@@ -904,7 +932,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
&& strtolower($error) == "honeypot") {
$mailManager = Drupal::service('plugin.manager.mail');
if (!$mailManager->mail("bos_email", 'alerts.honeypot', $recipient, "en", $item, NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")->warning(t("Email sending from Drupal has failed."));
+ Drupal::logger("bos_email:EmailController")->warning(t("Email sending from Drupal has failed."));
}
}
}
@@ -916,7 +944,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
if ($recipient) {
$mailManager = Drupal::service('plugin.manager.mail');
if (!$mailManager->mail("bos_email", 'monitor.all', $recipient, "en", array_merge($item, $response), NULL, TRUE)) {
- Drupal::logger("bos_email:PostmarkService")
+ Drupal::logger("bos_email:EmailController")
->warning(t("Email sending from Drupal has failed."));
}
}
@@ -925,7 +953,7 @@ public static function alertHandler($item, $response, $http_code, $config = NULL
// Do dome logging if this is a local dev environment.
if (str_contains(Drupal::request()->getHttpHost(), "lndo.site")) {
- Drupal::logger("bos_email:PostmarkService")
+ Drupal::logger("bos_email:EmailController")
->info("Email | " . json_encode($item) . " |
Response | " . json_encode($response ?? []) . " |
HTTPCode | {$http_code} |
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
index 64a310c988..82cd41a806 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailProcessorInterface.php
@@ -38,7 +38,7 @@ public static function fetchPayload(Request $request): array;
* Creates a message body for incoming emails.
*
* @param array $emailFields An array containing the fields supplied from a
- * Postmark webhook callback.
+ * webhook callback.
*
* @return void
*/
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
index 9ab310a47e..23c026e13e 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/EmailServiceInterface.php
@@ -27,9 +27,10 @@ public function updateEmailObject(CobEmail &$email_object): void;
*
* @param array $item Containing email fields for the service to send.
*
- * @returns bool Whether the send was successful or not.
+ * @returns void
+ * @throws \Exception
*/
- public function sendEmail(array $item): bool;
+ public function sendEmail(array $item): void;
/**
* Fetches any specific settings for this service.
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
index e89497e0ae..38a7988454 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Commissions.php
@@ -6,7 +6,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Commissions.
*/
class Commissions extends DefaultEmail implements EventSubscriberInterface {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
index 5e1a2d568d..51a20eee95 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Contactform.php
@@ -9,7 +9,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Contact Form.
*/
class Contactform extends EmailProcessorBase implements EventSubscriberInterface {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
index 89171f79f8..97cdc3fb78 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/DefaultEmail.php
@@ -7,7 +7,7 @@
use Drupal\Component\Utility\Xss;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Default Email.
*/
class DefaultEmail extends EmailProcessorBase {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
index 1077ac1c21..e05624d2c6 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistInitiationForm.php
@@ -7,7 +7,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Metrolist Initiation Form Emails.
*/
class MetrolistInitiationForm extends EmailProcessorBase implements EventSubscriberInterface {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
index 7351dfdb9b..051a235f89 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingConfirmation.php
@@ -3,10 +3,10 @@
namespace Drupal\bos_email\Plugin\EmailProcessor;
use Drupal\bos_email\CobEmail;
-use EmailProcessorBase;
+use Drupal\bos_email\Plugin\EmailProcessor\EmailProcessorBase;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Metrolist Listing Confirmation Emails.
*/
class MetrolistListingConfirmation extends EmailProcessorBase {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
index 10b0421131..d8cd591447 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/MetrolistListingNotification.php
@@ -5,7 +5,7 @@
use Drupal\bos_email\CobEmail;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for Metrolist Listing Notification emails.
*/
class MetrolistListingNotification extends EmailProcessorBase {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
index 46878f41fe..e67818f964 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Registry.php
@@ -7,7 +7,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
- * Template class for Postmark API.
+ * EmailProcessor class for registry emails.
*/
class Registry extends EmailProcessorBase implements EventSubscriberInterface {
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
index c4e81b44db..92f72c0195 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/EmailProcessor/Sanitation.php
@@ -9,7 +9,7 @@
use Symfony\Component\HttpFoundation\Request;
/**
- * Email Processor class for Sanitation Scheduling Emails.
+ * EmailProcessor class for Sanitation Scheduling Emails.
*/
class Sanitation extends EmailProcessorBase implements EventSubscriberInterface {
@@ -64,7 +64,7 @@ public static function parseEmailFields(array &$payload, CobEmail &$email_object
// Do the base email fields processing first.
parent::parseEmailFields($payload, $email_object);
- // Set up the Postmark template.
+ // Set up the sanitation template.
$template_id = \Drupal::config("bos_email.settings")->get("sanitation.template");
$email_object->setField("TemplateID", $template_id);
$email_object->setField("Tag", $payload["type"]);
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
index abad4676cc..dac7a0d105 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ContactformProcessItems.php
@@ -8,12 +8,12 @@
use Exception;
/**
- * Processes emails through Postmark API.
+ * Queue to hold unsent emails.
*
* @QueueWorker(
* id = "email_contactform",
- * title = @Translation("Sends emails through Postmark."),
- * cron = {"time" = 15}
+ * title = @Translation("Holds currently unsent emails."),
+ * cron = {"time" = 60}
* )
*/
class ContactformProcessItems extends QueueWorkerBase {
@@ -21,45 +21,56 @@ class ContactformProcessItems extends QueueWorkerBase {
/**
* Process each record.
*
- * @param mixed $item
+ * @param mixed $data
* The item stored in the queue.
*/
- public function processItem($item) {
+ public function processItem($data):void {
+
try {
$config = \Drupal::configFactory()->get("bos_email.settings");
if (!$config->get("q_enabled")) {
+ // All queue processing is disabled.
throw new \Exception("All queues are paused by settings at /admin/config/system/boston/email_services.");
}
- elseif (!empty($item["server"])
- && !$config->get(strtolower($item["server"]))["q_enabled"]) {
- throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
+ elseif (!empty($data["server"])
+ && !$config->get(strtolower($data["server"]))["q_enabled"]) {
+ // Just this queue processing for this EmailProcessor is disabled.
+ throw new \Exception("The queue for {$data["server"]} is paused by settings at /admin/config/system/boston/email_services.");
}
- if (!empty($item["send_error"])) {
- unset($item["send_error"]);
+ // Cleanup message before sending.
+ if (!empty($data["send_error"])) {
+ unset($data["send_error"]);
}
- if (!empty($item["service"])) {
+ // Load the original EmailService.
+ if (!empty($data["service"])) {
try {
- $email_ops = new $item["service"];
+ $emailService = new $data["service"];
}
catch (Exception $e) {}
}
-
- if (!isset($email_ops) || empty($email_ops)) {
- // Defaults to Drupal.
- $email_ops = new DrupalService();
+ if (empty($emailService)) {
+ // Defaults to Drupal so we can always send.
+ $emailService = new DrupalService();
}
- if (!$email_ops->sendEmail($item)) {
- throw new \Exception("There was a problem in {$email_ops->id()}. {$email_ops->error}");
+ try {
+ $emailService->sendEmail($data);
+ }
+ catch (Exception $e) {
+ // Bubble this exception. (the try/catch is not strictly necessary, but
+ // we can use it to add info for logging.)
+ \Drupal::logger("contactform")->error($e->getMessage());
+ throw new \Exception("There was a problem in {$emailService->id()}. {$e->getMessage()}");
}
}
catch (\Exception $e) {
- \Drupal::logger("contactform")->error($e->getMessage());
+ // Throwing an exception here will cause this email to be recycled back
+ // into the queue for immediate resending.
throw new \Exception($e->getMessage());
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
index 7364b11d49..3ac29f32ed 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Plugin/QueueWorker/ScheduledEmailProcessor.php
@@ -9,12 +9,12 @@
use Exception;
/**
- * Processes emails through Postmark API.
+ * Processes scheduled emails.
*
* @QueueWorker(
* id = "scheduled_email",
* title = @Translation("Queues emails for later sending."),
- * cron = {"time" = 15}
+ * cron = {"time" = 60}
* )
*/
class ScheduledEmailProcessor extends QueueWorkerBase {
@@ -22,67 +22,86 @@ class ScheduledEmailProcessor extends QueueWorkerBase {
/**
* Process each record.
*
- * @param mixed $item
+ * @param mixed $data
* The item stored in the queue.
*/
- public function processItem($item): void {
+ public function processItem($data): void {
try {
$config = \Drupal::configFactory()->get("bos_email.settings");
if (!$config->get("q_enabled")) {
- throw new \Exception("All queues are paused by settings at /admin/config/system/boston/email_services.");
+ // All queue processing is disabled.
+ throw new Exception("All queues are paused by settings at /admin/config/system/boston/email_services.");
}
- elseif (!empty($item["server"])
- && !$config->get(strtolower($item["server"]))["q_enabled"]) {
- throw new \Exception("The queue for {$item["server"]} is paused by settings at /admin/config/system/boston/email_services.");
+ elseif (!empty($data["server"])
+ && !$config->get(strtolower($data["server"]))["q_enabled"]) {
+ // Just this queue processing for this EmailProcessor is disabled.
+ throw new Exception("The queue for {$data["server"]} is paused by settings at /admin/config/system/boston/email_services.");
}
- if (intval($item["senddatetime"]) <= strtotime("Now")) {
- if (!empty($item["senddatetime"])) {
- unset($item["senddatetime"]);
+ // Check the scheduled send time for this email.
+ if (intval($data["senddatetime"]) <= strtotime("Now")) {
+ // The scheduled date/time has passed, so this email is now eligible to
+ // be sent.
+
+ // Tidy up the email first.
+ if (!empty($data["senddatetime"])) {
+ unset($data["senddatetime"]);
}
- if (!empty($item["send_date"])) {
- unset($item["send_date"]);
+ if (!empty($data["send_date"])) {
+ unset($data["send_date"]);
}
- // load the correct email service.
- if (!empty($item["service"])) {
+ // Load the original EmailService.
+ if (!empty($data["service"])) {
try {
- $email_ops = new $item["service"];
+ $emailService = new $data["service"];
}
catch (Exception $e) {}
}
-
- if (empty($email_ops)) {
- // Defaults to Drupal so we can always send an email.
- $email_ops = new DrupalService();
+ if (empty($emailService)) {
+ // Defaults to sending via Drupal so that we can always send an email.
+ $emailService = new DrupalService();
}
- // This will throw an error if the mail does not send, and will cause
- // the item to remain in the queue.
- $send = $email_ops->sendEmail($item);
+ // This will throw an error if the mail does not send, which will cause
+ // the selected email to remain in the queue.
+ // Failures in sending will recycle the email back into this queue, and
+ // make it eligible for immediate resending.
+ try {
+ $emailService->sendEmail($data);
+ }
+ catch (Exception $e) {
+ // Bubble this exception. (the try/catch is not strictly necessary, but
+ // we can use it to add info for logging.)
+ \Drupal::logger("contactform")->error($e->getMessage());
+ throw new Exception("There was a problem in {$emailService->id()}. {$e->getMessage()}");
+ }
}
else {
- // Delay the resending of this email until its scheduled time.
+ // This email is not ready to be sent yet because the send time has not
+ // been reached. Throw this error to put the email back into this queue
+ // and not make it available until its scheduled time.
throw new DelayedRequeueException(
- intval($item["senddatetime"]) - strtotime("Now"),
+ intval($data["senddatetime"]) - strtotime("Now"),
"Email scheduled for the future."
);
}
- if (!$send) {
- throw new \Exception("There was a problem in {$email_ops->id()}. {$email_ops->error}");
- }
-
}
catch (DelayedRequeueException $e) {
+ // This has most likely been thrown in the normal course of events because
+ // it is not the scheduled time to send this email yet.
+ // Throwing this error will put the email back in this queue and will not
+ // allow any worker to select it again until the time it should be sent.
throw new DelayedRequeueException($e->getDelay(), $e->getMessage());
}
- catch (\Exception $e) {
- \Drupal::logger("contactform")->error($e->getMessage());
- throw new \Exception($e->getMessage());
+ catch (Exception $e) {
+ // This is a general error, so simply put the email back into this queue
+ // and mark it eligible for immediate resending.
+ throw new Exception($e->getMessage());
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
index 9f5eaf578e..ef381baa1c 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/DrupalService.php
@@ -8,9 +8,10 @@
use Drupal\Core\Render\Markup;
use Drupal\Core\Site\Settings;
use Drupal\bos_email\CobEmail;
+use Exception;
/**
- * Postmark class for API.
+ * EmailService class for sedning via DrupalMail .
*/
class DrupalService implements EmailServiceInterface {
@@ -49,30 +50,17 @@ public function updateEmailObject(CobEmail &$email_object): void {
}
/**
- * Send the email via Postmark.
+ * Send the email via Drupal.
*
- * @param \Drupal\bos_email\CobEmail $mailobj The email object
+ * @param array $item
*
- * @return array
+ * @return void
+ * @throws Exception
*/
- public function sendEmail(array $item):bool {
-
- // Check if we are sending out emails.
- $config = Drupal::configFactory()->get("bos_email.settings");
- if (!$config->get("enabled")) {
- $this->error = "Emailing temporarily suspended for all emails";
- Drupal::logger("bos_email:DrupalService")->error($this->error);
- return FALSE;
- }
- elseif ($item["server"] && !$config->get(strtolower($item["server"]))["enabled"]) {
- $this->error = "Emailing temporarily suspended for {$item["server"]} emails.";
- Drupal::logger("bos_email:DrupalService")->error($this->error);
- return FALSE;
- }
+ public function sendEmail(array $item):void {
+
+ $this->error = NULL;
- /**
- * @var \Drupal\Core\Mail\MailManager $mailManager
- */
try {
// Send the email.
@@ -81,6 +69,7 @@ public function sendEmail(array $item):bool {
$mailManager = Drupal::service('plugin.manager.mail');
$sent = $mailManager->mail("bos_email", $item["server"] , $item["To"], "en", $item, $item["ReplyTo"], TRUE);
+ // Put something into the response object.
$this->response = [
"sent" => $sent ? "True" : "False",
];
@@ -88,25 +77,21 @@ public function sendEmail(array $item):bool {
if (!$sent || !$sent["result"]) {
if (!empty($params["_error_message"])) {
$this->response["error"] = $params["_error_message"];
- throw new \Exception($params["_error_message"]);
+ $this->error = $this->response["error"];
+ throw new Exception($this->error);
}
else {
$this->response["error"] = "Error sending email";
- throw new \Exception("Error sending email.");
+ $this->error = $this->response["error"];
+ throw new Exception($this->error);
}
}
- return TRUE;
-
}
catch (\Exception $e) {
- $this->error = $e->getMessage();
- $this->response["error"] = $this->error;
- if (Boston::is_local()) {
- Drupal::logger("bos_email:DrupalService")
- ->info("Queued {$item["server"]}");
- return FALSE;
- }
+ $this->response["error"] = $e->getMessage();
+ $this->error = $this->response["error"];
+ throw new Exception($this->error);
}
@@ -117,21 +102,21 @@ public function sendEmail(array $item):bool {
*/
public function getVars(): array {
- $postmark_env = [];
+ $drupal_env = [];
if (getenv('POSTMARK_SETTINGS')) {
$get_vars = explode(",", getenv('POSTMARK_SETTINGS'));
foreach ($get_vars as $item) {
$json = explode(":", $item);
if (!empty($json[0]) && !empty($json[1])) {
- $postmark_env[$json[0]] = $json[1];
+ $drupal_env[$json[0]] = $json[1];
}
}
}
else {
- $postmark_env = Settings::get('postmark_settings') ?? [];
+ $drupal_env = Settings::get('postmark_settings') ?? [];
}
- return $postmark_env;
+ return $drupal_env;
}
/**
diff --git a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
index 7bef0a848e..ea7b2de120 100644
--- a/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
+++ b/docroot/modules/custom/bos_components/modules/bos_email/src/Services/PostmarkService.php
@@ -9,7 +9,7 @@
use Exception;
/**
- * Postmark variables for email API.
+ * EmailService class for sedning via PostMark.
*/
class PostmarkService extends BosCurlControllerBase implements EmailServiceInterface {
@@ -89,21 +89,13 @@ public function updateEmailObject(CobEmail &$email_object): void {
/**
* Send email to Postmark.
+ *
+ * @param array $item
+ * @throws Exception
*/
- public function sendEmail(array $item):bool {
-
- // Check if we are sending out emails.
- $config = \Drupal::configFactory()->get("bos_email.settings");
- if (!$config->get("enabled")) {
- $this->error = "Emailing temporarily suspended for all emails";
- \Drupal::logger("bos_email:PostmarkService")->error($this->error);
- return FALSE;
- }
- elseif ($item["server"] && !$config->get(strtolower($item["server"]))["enabled"]) {
- $this->error = "Emailing temporarily suspended for {$item["server"]} emails.";
- \Drupal::logger("bos_email:PostmarkService")->error($this->error);
- return FALSE;
- }
+ public function sendEmail(array $item):void {
+
+ $this->error = NULL;
try {
$server_token = $item["server"] . "_token";
@@ -141,12 +133,10 @@ public function sendEmail(array $item):bool {
throw new \Exception("Return Error Code: {$this->response["http_code"]}
HEADERS: {$headers}
PAYLOAD: {$item}
RESPONSE:{$response}");
}
- return TRUE;
-
}
catch (Exception $e) {
$this->error = $e->getMessage();
- return FALSE;
+ throw new Exception($this->error);
}
}
From 7a8ece1d01cb06932620ab26b6f31d463e347e9e Mon Sep 17 00:00:00 2001
From: David Upton
Date: Thu, 25 Apr 2024 23:55:49 -0400
Subject: [PATCH 10/48] DIG-4317 config ignore bos_email settings
---
config/acquia_dev/config_ignore.settings.yml | 1 +
config/acquia_stage/config_ignore.settings.yml | 1 +
config/local/config_ignore.settings.yml | 1 +
3 files changed, 3 insertions(+)
diff --git a/config/acquia_dev/config_ignore.settings.yml b/config/acquia_dev/config_ignore.settings.yml
index 05bc18d297..f6a0edefda 100644
--- a/config/acquia_dev/config_ignore.settings.yml
+++ b/config/acquia_dev/config_ignore.settings.yml
@@ -16,3 +16,4 @@ ignored_config_entities:
- slackposter.settings
- node_elections.settings
- block.block.website_feedback_form
+ - bos_email.settings
diff --git a/config/acquia_stage/config_ignore.settings.yml b/config/acquia_stage/config_ignore.settings.yml
index 2bf8e3317c..3b6c28dd4d 100644
--- a/config/acquia_stage/config_ignore.settings.yml
+++ b/config/acquia_stage/config_ignore.settings.yml
@@ -16,4 +16,5 @@ ignored_config_entities:
- slackposter.settings
- node_elections.settings
- block.block.website_feedback_form
+ - bos_email.settings
diff --git a/config/local/config_ignore.settings.yml b/config/local/config_ignore.settings.yml
index e2b24d3ebf..e11836670f 100644
--- a/config/local/config_ignore.settings.yml
+++ b/config/local/config_ignore.settings.yml
@@ -30,3 +30,4 @@ ignored_config_entities:
- node_elections.settings
- node_rollcall.settings
- slackposter.settings
+ - bos_email.settings
From 4b8e0a26c2767459e8534714e22c142787d37f87 Mon Sep 17 00:00:00 2001
From: David Upton
Date: Mon, 29 Apr 2024 22:30:46 -0400
Subject: [PATCH 11/48] DIG-4213 Special Item collection boston.gov page for
widget embed
---
.../apps/sanitation_scheduling/README.md | 11 +++
.../apps/sanitation_scheduling/composer.json | 39 ++++++++
.../sanitation_scheduling/docker-compose.yml | 37 ++++++++
.../sanitation_scheduling.info.yml | 8 ++
.../sanitation_scheduling.libraries.yml | 19 ++++
.../sanitation_scheduling.module | 30 +++++++
.../modules/bos_web_app/bos_web_app.module | 16 +++-
.../modules/bos_web_app/images/spinner.gif | Bin 0 -> 51107 bytes
.../modules/bos_web_app/images/spinner_2.gif | Bin 0 -> 19264 bytes
.../modules/bos_web_app/readme.md | 84 ++++++++++++++++++
.../templates/paragraph--web-app.html.twig | 24 +++--
.../bos_theme/css/bos_theme_overrides.css | 1 +
scripts/deploy/travis_build.sh | 1 +
13 files changed, 263 insertions(+), 7 deletions(-)
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/README.md
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/composer.json
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/docker-compose.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.info.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.libraries.yml
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.module
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner.gif
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner_2.gif
create mode 100644 docroot/modules/custom/bos_components/modules/bos_web_app/readme.md
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/README.md b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/README.md
new file mode 100644
index 0000000000..818a95598f
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/README.md
@@ -0,0 +1,11 @@
+# Sanitation Scehduling React App
+
+## Build
+From the sanitation_scheduling folder, ensure the command in `docker-compose.yml` is "build" and then run `docker-compose up sanitation`. This will create the necessary dist files in the `sanitation_scheduling/app/frontend/dist/assets` folder.
+
+## Deploy
+### To develop
+Commit changes to the files in `sanitation_scheduling/app/frontend/src` into the repository `develop` branch. Github actions will run to build and deploy the app into Google Firebase. This Drupal module does not normally need to be updated or deployed.
+
+### To Production
+Merge the updated `develop` branch into `production`. Github actions will run to build and deploy the app into Google Firebase. This Drupal module does not normally need to be updated.
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/composer.json b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/composer.json
new file mode 100644
index 0000000000..eebd0699bb
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "cob-webapp/sanitation_scheduling",
+ "description": "React app hosted on Boston.gov allowing residents to schedule Mattress and Bulk item collection.",
+ "license": "GPL-2.0-or-later",
+ "type": "project",
+ "version": "1.0",
+ "authors": [
+ {
+ "name": "Speedlane",
+ "role": "Frontend Development Consultancy",
+ "email": "maroun.melhem@boston.gov"
+ },
+ {
+ "name": "David Upton",
+ "role": "Drupal Developer.",
+ "email": "david.upton@boston.gov"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "package",
+ "package": {
+ "name": "bos_web_app/sanitation_scheduling",
+ "version": "1.02",
+ "dist": {
+ "url": "https://github.com/CityOfBoston/sanitation-scheduling/archive/1.0.zip",
+ "type": "zip"
+ },
+ "type": "drupal-custom-module",
+ "source": {
+ "url": "git@github.com:CityOfBoston/sanitation-scheduling.git",
+ "type": "git",
+ "reference": "1053dc5"
+ }
+ }
+ }
+ ]
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/docker-compose.yml b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/docker-compose.yml
new file mode 100644
index 0000000000..b45da86743
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/docker-compose.yml
@@ -0,0 +1,37 @@
+version: '3.6'
+
+networks:
+ cob_docker_network:
+ driver: bridge
+ name: cob_docker_network
+
+services:
+ sanitation:
+ build:
+ context: .
+ dockerfile: dockerfile
+ container_name: cob-sanitation
+ image: 'node:18-bullseye'
+ networks:
+ - cob_docker_network
+ expose:
+ - 5173
+ entrypoint: "/docker-entrypoint.sh"
+# command: "run"
+ command: "build"
+ environment:
+ GIT_REPO: "CityOfBoston/sanitation-scheduling.git"
+ GIT_SSH_CERT_FILE: "/hostuser/.ssh/id_rsa"
+ GIT_BRANCH: "main"
+ ports:
+ - '5173:5173'
+ volumes:
+ - ./:/sanitation-scheduler
+ - ./docker-entrypoint.sh:/docker-entrypoint.sh
+ - ~/.ssh/known_hosts:/root/.ssh/known_hosts
+ - ~:/hostuser
+ labels:
+ traefik.enable: true
+ traefik.http.routers.digitalapps.rule: "Host(`sanitation.docker.localhost`)"
+ traefik.http.routers.digitalapps.entrypoints: "http"
+ traefik.http.services.digitalapps.loadbalancer.server.port: "5173"
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.info.yml b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.info.yml
new file mode 100644
index 0000000000..9f1629a76d
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.info.yml
@@ -0,0 +1,8 @@
+name: 'sanitation_scheduling'
+type: module
+description: 'React app hosted on Boston.gov allowing residents to schedule Mattress and Bulk item collection'
+core_version_requirement: ^10
+package: 'Custom'
+dependencies:
+ - bos_web_app
+config_devel: { }
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.libraries.yml b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.libraries.yml
new file mode 100644
index 0000000000..6396773aa3
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.libraries.yml
@@ -0,0 +1,19 @@
+sanitation_scheduling-dev:
+ version: scheduling.123456
+ js:
+ app/frontend/dist/assets/bundle.js: {preprocess: false, attributes: {type: 'module', id: sanitation-js}}
+ css:
+ layout:
+ app/frontend/dist/assets/index.css: {preprocess: false, attributes: {media: screen, type: text/css}}
+ dependencies:
+ - core/drupalSettings
+
+sanitation_scheduling:
+ version: scheduling.123456
+ js:
+ https://sanitation-scheduling-dev.web.app/assets/bundle.js: {preprocess: false, attributes: {type: 'module', id: sanitation-js}}
+ css:
+ layout:
+ https://sanitation-scheduling-dev.web.app/assets/index.css: {preprocess: false, attributes: {media: screen, type: text/css}}
+ dependencies:
+ - core/drupalSettings
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.module b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.module
new file mode 100644
index 0000000000..b538186d6a
--- /dev/null
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/apps/sanitation_scheduling/sanitation_scheduling.module
@@ -0,0 +1,30 @@
+setAttribute("id", "sanitation-scheduling-app");
+
+ // This optional command will cause a script function to run after the page
+ // is loaded.
+ // Any valid javascript added to $vars["autorun"] will be executed.
+// $vars["autorun"] = 'alert("Will load sanitation app");';
+
+ }
+
+}
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/bos_web_app.module b/docroot/modules/custom/bos_components/modules/bos_web_app/bos_web_app.module
index c414e12b0e..090284a097 100644
--- a/docroot/modules/custom/bos_components/modules/bos_web_app/bos_web_app.module
+++ b/docroot/modules/custom/bos_components/modules/bos_web_app/bos_web_app.module
@@ -24,13 +24,25 @@ function bos_web_app_theme() {
*/
function bos_web_app_preprocess_paragraph__web_app(array &$vars) {
if (!empty($vars['paragraph'])) {
- $paragraph = $vars['paragraph'];
+
+ // Include a library for the bos_web_app module itself.
$vars['#attached']['library'][] = 'bos_web_app/bos_web_app';
+
+ // Legacy, include a library ref for the app name which is defined in the
+ // bos_web_app module.
+ // NOTE: Library inclusion should be handled in the apps own module.
+ $paragraph = $vars['paragraph'];
if ($paragraph->hasField('field_app_name')) {
+ $libraryDiscovery = \Drupal::service('library.discovery');
+ $libraries = $libraryDiscovery->getLibrariesByExtension("bos_web_app");
$app_name = strtolower($paragraph->get('field_app_name')->value);
$app_name = str_replace(' ', '_', $app_name);
- $vars['#attached']['library'][] = 'bos_web_app/' . $app_name;
+ if (isset($libraries[$app_name])) {
+ $vars['#attached']['library'][] = 'bos_web_app/' . $app_name;
+ }
}
+ $vars['module_path'] = \Drupal::service('module_handler')->getModule('bos_web_app')->getPath();
+
}
}
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner.gif b/docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2fde2f7f31b17f252040ffd989ed05151bc88ac8
GIT binary patch
literal 51107
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
literal 0
HcmV?d00001
diff --git a/docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner_2.gif b/docroot/modules/custom/bos_components/modules/bos_web_app/images/spinner_2.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2b2a9a4cc57eb9130981d11a0a3e200cdae6d702
GIT binary patch
literal 19264
zcmd_SX;72ty0-l!c`}FO$w&xENWzRrqpdU|+F>3prQsu
z5ODwmK}89Jh=PhZA<`qN449W83=Pf!6I>d-d91)#|tEt#A9If3#iQ)#Z`%KCk1t
zh?9$><&+R9B!zB3Q1OYDj(fv*Uw^K?cz5vW@W{K7mYajuAB_yZ9%;FJ_w$#})vedB
z-y0l$G4lD-=Z>3q?>-tH{`9%{Y{%fc&-Gn{gD*x#K7Ag3_xbMA;f{fkyWnqLygvAJ
zWaQoFmY(6^*PmOu?p_}l?&ujje7586(~+-Vze0cOFY#CMEC(-7I~#Y0iDu>)6!@2m
zKf**jqz{>@|3m8kO$hfzl@;M^%G=&n9C;z!YG3EUDAywa$GQ)%ZFdMTft_lXxj>g8
z*N}pmEuBM|NgF0NUuDJKh-|p>)`PtvLQn2Uf^i}V!4XafOG>umXfqhP`4W%c$CPzAxUJxMKS<=klJ8&>{i!p^x944Bl87L}&
zc6RF`PMUh?>n
z2IQC&+y{r86Pfg-fgZ^``lYm~
z23p8