Skip to content

Commit

Permalink
Merge branch '2.x' into develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
#	composer.json
#	src/services/ReportsService.php
  • Loading branch information
bencroker committed Apr 14, 2024
2 parents 4ba644e + 1716ace commit 18f11f9
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 29 deletions.
15 changes: 9 additions & 6 deletions src/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,23 @@
// An API key to use for triggering tasks and notifications (min. 16 characters)
//'apiKey' => 'aBcDeFgHiJkLmNoP',

//Whether to validate incoming webhook requests using a verification key
//validateWebhookRequests => true,
// Whether one-click unsubscribe headers should be added to emails.
//'addOneClickUnsubscribeHeaders' => true,

// Whether to validate incoming webhook requests using a verification key
//'validateWebhookRequests' => true,

// A webhook signing secret provided by MailerSend to validate incoming webhook requests
//mailersendWebhookSigningSecret => 'aBcDeFgHiJkLmNoP123',
//'mailersendWebhookSigningSecret' => 'aBcDeFgHiJkLmNoP123',

// A webhook signing key provided by Mailgun to validate incoming webhook requests
//mailgunWebhookSigningKey => 'key-aBcDeFgHiJkLmNoP',
//'mailgunWebhookSigningKey' => 'key-aBcDeFgHiJkLmNoP',

// A webhook verification key provided by SendGrid to validate incoming webhook requests
//sendgridWebhookVerificationKey => 'aBcDeFgHiJkLmNoP123==',
//'sendgridWebhookVerificationKey' => 'aBcDeFgHiJkLmNoP123==',

// The allowed IP addresses for incoming webhook requests from Postmark
//postmarkAllowedIpAddresses => [
//'postmarkAllowedIpAddresses' => [
// '3.134.147.250',
// '50.31.156.6',
// '50.31.156.77',
Expand Down
36 changes: 36 additions & 0 deletions src/controllers/TrackerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@

class TrackerController extends BaseMessageController
{
/**
* @inheritdoc
*/
public $enableCsrfValidation = false;

/**
* @var bool Disable Snaptcha validation
*/
public bool $enableSnaptchaValidation = false;

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -120,6 +130,32 @@ public function actionUnsubscribe(): ?Response
]);
}

/**
* Tracks a one-click unsubscribe.
* https://postmarkapp.com/support/article/1299-how-to-include-a-list-unsubscribe-header
*
* @since 2.15.0
*/
public function actionOneClickUnsubscribe(): ?Response
{
// Ignore if a non-POST requests but don’t require it, since anti-spam tools may send GET requests.
if (!$this->request->getIsPost()) {
return $this->asRaw('');
}

// Get contact and sendout
$contact = $this->getContact();
$sendout = $this->getSendout();

if ($contact === null || $sendout === null) {
throw new NotFoundHttpException(Craft::t('campaign', 'Unsubscribe link is invalid.'));
}

Campaign::$plugin->tracker->unsubscribe($contact, $sendout);

return $this->asRaw('OK');
}

/**
* Gets a contact by CID in param.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/elements/ContactElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ public function getMailingListSubscriptionStatus(int $mailingListId): string
{
/** @var ContactMailingListRecord|null $contactMailingList */
$contactMailingList = ContactMailingListRecord::find()
->select('subscriptionStatus')
->select(['subscriptionStatus'])
->where([
'contactId' => $this->id,
'mailingListId' => $mailingListId,
Expand Down Expand Up @@ -1075,7 +1075,7 @@ private function getMailingListsWithStatus(string $subscriptionStatus = null): a

/** @var ContactMailingListRecord[] $contactMailingLists */
$contactMailingLists = ContactMailingListRecord::find()
->select('mailingListId')
->select(['mailingListId'])
->where($condition)
->all();

Expand Down
2 changes: 1 addition & 1 deletion src/elements/MailingListElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ private function getContactCount(string $subscriptionStatus = null): int
private function getContactsBySubscriptionStatus(string $subscriptionStatus = null): array
{
$query = ContactMailingListRecord::find()
->select('contactId')
->select(['contactId'])
->where(['mailingListId' => $this->id]);

if ($subscriptionStatus) {
Expand Down
2 changes: 1 addition & 1 deletion src/elements/SendoutElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ public function getNotificationEmailAddresses(): array
}

return ContactElement::find()
->select('email')
->select(['email'])
->id($this->notificationContactIds)
->column();
}
Expand Down
8 changes: 4 additions & 4 deletions src/elements/db/CampaignElementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ protected function beforePrepare(): bool

// Add the last sent date
$sendoutQuery = SendoutRecord::find()
->select('campaignId, MAX([[lastSent]]) AS lastSent')
->groupBy('campaignId');
->select(['campaignId', 'lastSent' => 'MAX([[lastSent]])'])
->groupBy(['campaignId']);

$this->query->addSelect('lastSent');
$this->query->addSelect(['lastSent']);
$this->subQuery->leftJoin(['campaign_sendouts' => $sendoutQuery], '[[campaign_sendouts.campaignId]] = [[campaign_campaigns.id]]');
$this->subQuery->select('campaign_sendouts.lastSent AS lastSent');
$this->subQuery->select(['lastSent' => 'campaign_sendouts.lastSent']);

// Filter by campaign types in sites that have not been deleted
$this->subQuery->innerJoin(CampaignTypeRecord::tableName() . ' campaign_campaigntypes', '[[campaign_campaigntypes.id]] = [[campaign_campaigns.campaignTypeId]]');
Expand Down
4 changes: 2 additions & 2 deletions src/elements/db/ContactElementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ protected function beforePrepare(): bool
}

if ($this->mailingListId) {
$this->query->addSelect('subscriptionStatus');
$this->query->addSelect(['subscriptionStatus']);
$this->subQuery->innerJoin(ContactMailingListRecord::tableName() . ' campaign_contacts_mailinglists', '[[campaign_contacts.id]] = [[campaign_contacts_mailinglists.contactId]]');
$this->subQuery->select('campaign_contacts_mailinglists.subscriptionStatus AS subscriptionStatus');
$this->subQuery->select(['subscriptionStatus' => 'campaign_contacts_mailinglists.subscriptionStatus']);
$this->subQuery->andWhere(Db::parseParam('campaign_contacts_mailinglists.mailingListId', $this->mailingListId));
} else {
// Add a dummy subscriptionStatus value to prevent sorted queries from failing
Expand Down
2 changes: 1 addition & 1 deletion src/elements/db/MailingListElementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ protected function beforePrepare(): bool
}

$this->subQuery->innerJoin(MailingListTypeRecord::tableName() . ' campaign_mailinglisttypes', '[[campaign_mailinglisttypes.id]] = [[campaign_mailinglists.mailingListTypeId]]');
$this->subQuery->select('campaign_mailinglisttypes.name AS mailingListType');
$this->subQuery->select(['mailingListType' => 'campaign_mailinglisttypes.name']);

$this->subQuery->innerJoin(Table::SITES . ' sites', '[[sites.id]] = [[campaign_mailinglisttypes.siteId]]');
$this->subQuery->andWhere(['[[sites.dateDeleted]]' => null]);
Expand Down
6 changes: 6 additions & 0 deletions src/models/SettingsModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class SettingsModel extends Model
*/
public string $apiKey;

/**
* @var bool Whether one-click unsubscribe headers should be added to emails.
* @since 2.15.0
*/
public bool $addOneClickUnsubscribeHeaders = true;

/**
* @var bool Whether to validate incoming webhook requests using a signing key or secret
* @since 2.10.0
Expand Down
28 changes: 19 additions & 9 deletions src/services/SendoutsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use craft\helpers\Queue;
use craft\helpers\StringHelper;
use craft\helpers\UrlHelper;
use craft\mail\Message;
use DateTime;
use DOMDocument;
use DOMElement;
Expand Down Expand Up @@ -87,7 +88,7 @@ public function getSendoutBySid(string $sid): ?SendoutElement
}

$sendoutId = SendoutRecord::find()
->select('id')
->select(['id'])
->where(['sid' => $sid])
->scalar();

Expand Down Expand Up @@ -299,9 +300,18 @@ public function sendEmail(SendoutElement $sendout, ContactElement $contact, int
Campaign::$plugin->mailer->useFileTransport = true;
}

// Create message
$message = Campaign::$plugin->mailer->compose()
->setFrom([$sendout->fromEmail => $sendout->fromName])
/** @var Message $message */
$message = Campaign::$plugin->mailer->compose();

if (Campaign::$plugin->settings->addOneClickUnsubscribeHeaders) {
// Use the one-click unsubscribe controller action.
$oneClickUnsubscribeUrl = str_replace('campaign/t/unsubscribe', 'campaign/t/one-click-unsubscribe', $contact->getUnsubscribeUrl($sendout));

$message->setHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click')
->setHeader('List-Unsubscribe', $oneClickUnsubscribeUrl);
}

$message->setFrom([$sendout->fromEmail => $sendout->fromName])
->setTo($contact->email)
->setSubject($sendout->subject)
->setHtmlBody($htmlBody)
Expand Down Expand Up @@ -543,7 +553,7 @@ private function getMailingListById(int $mailingListId): MailingListElement
private function getExcludedMailingListRecipientsQuery(SendoutElement $sendout): ActiveQuery
{
return ContactMailingListRecord::find()
->select('contactId')
->select(['contactId'])
->where([
'mailingListId' => $sendout->excludedMailingListIds,
'subscriptionStatus' => 'subscribed',
Expand All @@ -556,7 +566,7 @@ private function getExcludedMailingListRecipientsQuery(SendoutElement $sendout):
private function getSentRecipientsQuery(SendoutElement $sendout, bool $todayOnly = false): ActiveQuery
{
$query = ContactCampaignRecord::find()
->select('contactId')
->select(['contactId'])
->where(['sendoutId' => $sendout->id])
->andWhere(['not', ['sent' => null]]);

Expand Down Expand Up @@ -594,7 +604,7 @@ private function getPendingRecipientsStandardIds(SendoutElement $sendout): array

// Get contacts subscribed to sendout’s mailing lists
$query = ContactMailingListRecord::find()
->select('contactId')
->select(['contactId'])
->andWhere($baseCondition);

// Ensure contacts have not complained, bounced, or been blocked (in contact record)
Expand Down Expand Up @@ -648,8 +658,8 @@ private function getPendingRecipientsStandard(SendoutElement $sendout, int $limi

/** @var array $recipients */
$recipients = ContactMailingListRecord::find()
->select(['contactId', 'min([[mailingListId]]) as mailingListId', 'min([[subscribed]]) as subscribed'])
->groupBy('contactId')
->select(['contactId', 'mailingListId' => 'MIN([[mailingListId]])', 'subscribed' => 'MIN([[subscribed]])'])
->groupBy(['contactId'])
->where($baseCondition)
->andWhere(['contactId' => $contactIds])
->orderBy(['contactId' => SORT_ASC])
Expand Down
6 changes: 3 additions & 3 deletions tests/pest/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,13 @@ function cleanup(): void
{
$campaignType = Campaign::$plugin->campaignTypes->getCampaignTypeByHandle(App::env('TEST_CAMPAIGN_TYPE_HANDLE'));
$campaignIds = CampaignRecord::find()
->select('id')
->select(['id'])
->where(['campaignTypeId' => $campaignType->id])
->column();

$mailingListType = Campaign::$plugin->mailingListTypes->getMailingListTypeByHandle(App::env('TEST_MAILING_LIST_TYPE_HANDLE'));
$mailingListIds = MailingListRecord::find()
->select('id')
->select(['id'])
->where(['mailingListTypeId' => $mailingListType->id])
->column();

Expand All @@ -153,7 +153,7 @@ function cleanup(): void

$userGroup = Craft::$app->userGroups->getGroupByHandle(App::env('TEST_USER_GROUP_HANDLE'));
$userIds = User::find()
->select('id')
->select(['id'])
->groupId($userGroup->id)
->column();

Expand Down

0 comments on commit 18f11f9

Please sign in to comment.