Elastic Emails Bounces are not properly handled

VVT

Active Member
Hi @twisted1919 ,

I just found that a wide category of soft bounces are being marked as unsubscribes when elastic email webhooks are used.

Here's the webhook list : https://elasticemail.com/support/delivery/http-web-notification
Here's its explanation : https://elasticemail.com/support/user-interface/activity/bounced-category-filters

While comparing the stats, you can find that only "No Mailbox" and "Account Problem" should be considered as hard bounces, all others fall in to soft category.

But, hard bounces, soft bounces and unsubscribes all are mishandled in this file : /apps/frontend/controllers/DswhController.ph

PHP:
 if (in_array($category, array('Ignore', 'DNSProblem', 'NotDelivered', 'NoMailbox', 'AccountProblem', 'ConnectionTerminated', 'ContentFilter'))
) {
            $softBounce = in_array($category, array('AccountProblem', 'ConnectionTerminated', 'ContentFilter'));
            $bounceLog = new CampaignBounceLog();
            $bounceLog->campaign_id     = $campaign->campaign_id;
            $bounceLog->subscriber_id   = $subscriber->subscriber_id;
            $bounceLog->message         = 'BOUNCED BACK: ' . $category;
            $bounceLog->bounce_type     = $softBounce ? CampaignBounceLog::BOUNCE_SOFT : CampaignBounceLog::BOUNCE_HARD;
            $bounceLog->save();

            if ($bounceLog->bounce_type == CampaignBounceLog::BOUNCE_HARD) {
                $subscriber->addToBlacklist($bounceLog->message);
            }
            Yii::app()->end();
        }

        if ($category == 'Spam' || ($category == 'Unknown' && $status == 'AbuseReport')) {
            if (Yii::app()->options->get('system.cron.process_feedback_loop_servers.subscriber_action', 'unsubscribe') == 'delete') {
                $subscriber->delete();
                Yii::app()->end();
            }

You can see the category "Spam" is treated as unsubscribe, but actually it's a soft bounce. Also, handles are not available for all the categories (may be they added them after you wrote the extension).

The biggest problem I face is, instead of soft bounces, those are unsubscribed from the list. So, I request you to fix this as soon as possible :(
 
Hi @twisted1919 ,

I just found that a wide category of soft bounces are being marked as unsubscribes when elastic email webhooks are used.

Here's the webhook list : https://elasticemail.com/support/delivery/http-web-notification
Here's its explanation : https://elasticemail.com/support/user-interface/activity/bounced-category-filters

While comparing the stats, you can find that only "No Mailbox" and "Account Problem" should be considered as hard bounces, all others fall in to soft category.

But, hard bounces, soft bounces and unsubscribes all are mishandled in this file : /apps/frontend/controllers/DswhController.ph

PHP:
 if (in_array($category, array('Ignore', 'DNSProblem', 'NotDelivered', 'NoMailbox', 'AccountProblem', 'ConnectionTerminated', 'ContentFilter'))
) {
            $softBounce = in_array($category, array('AccountProblem', 'ConnectionTerminated', 'ContentFilter'));
            $bounceLog = new CampaignBounceLog();
            $bounceLog->campaign_id     = $campaign->campaign_id;
            $bounceLog->subscriber_id   = $subscriber->subscriber_id;
            $bounceLog->message         = 'BOUNCED BACK: ' . $category;
            $bounceLog->bounce_type     = $softBounce ? CampaignBounceLog::BOUNCE_SOFT : CampaignBounceLog::BOUNCE_HARD;
            $bounceLog->save();

            if ($bounceLog->bounce_type == CampaignBounceLog::BOUNCE_HARD) {
                $subscriber->addToBlacklist($bounceLog->message);
            }
            Yii::app()->end();
        }

        if ($category == 'Spam' || ($category == 'Unknown' && $status == 'AbuseReport')) {
            if (Yii::app()->options->get('system.cron.process_feedback_loop_servers.subscriber_action', 'unsubscribe') == 'delete') {
                $subscriber->delete();
                Yii::app()->end();
            }

You can see the category "Spam" is treated as unsubscribe, but actually it's a soft bounce. Also, handles are not available for all the categories (may be they added them after you wrote the extension).

The biggest problem I face is, instead of soft bounces, those are unsubscribed from the list. So, I request you to fix this as soon as possible :(

meanwhile you could try with the traditional bounce box instead api/webhook tech
(this gives you full custom control how bounces are handled, incl custom rules)
 
  • Like
Reactions: VVT
But for that I need to turn off web notifications and that won't feed back the abuse reports. I think I'll wait for @twisted1919 to adjust the file.
Meanwhile, I'll be removing the spam category from unsubscribe portion.

@twisted1919 ,
Also, the category name is "Spam" not "SPam" , as mentioned in EE website. (Meaning, what you have in the file is the correct category name)
 
Logically, the following change in the above script should work, or at least this is the logical way it should work.


PHP:
if (in_array($category, array('Ignore','Spam','BlackListed','NoMailbox','GreyListed','Throttled','Timeout','ConnectionProblem','SPFProblem','AccountProblem','DNSProblem','WhitelistingProblem','CodeError','ManualCancel','ConnectionTerminated','ContentFilter','NotDelivered','Unknown'))
) {
            $hardBounce = in_array($category, array('AccountProblem','NoMailbox'));
---
$bounceLog->bounce_type = $hardBounce ? CampaignBounceLog::BOUNCE_HARD : CampaignBounceLog::BOUNCE_SOFT;
---
if ($category == 'Unknown' && $status == 'AbuseReport') {
--
 
@VVT - Here's the thing, i can understand that : 'BlackListed', 'NoMailbox', 'AccountProblem' should be hard bounces, make sense.
But to mark a subscriber as soft bounce because of: 'GreyListed', 'DNSProblem', 'WhitelistingProblem', 'CodeError', 'ManualCancel' etc doesn't make any sense. These are server errors, they have nothing to do with the subscriber itself, so we need to change the logic here.

Another note is that SPAM should not be a hard/soft bounce according to it's definition from the link you gave: "The email was rejected because it matched a profile the internet community has labeled as Spam."
We should not mark a subscriber as soft/hard bounce in this case because the subscriber address is fine, the problem is the filter before the email reaches the subscriber. Maybe sending with another delivery server will land the email inbox, we should not punish the subscriber for this. makes sense?

Now, given the above, here's the current logic:
1. When the category is Unknown and the status is AbuseReport, then this is clearly a unsubscribe action in mailwizz.
2. When the category is any of: 'BlackListed', 'NoMailbox', 'AccountProblem' then it is a hard bounce.
3. When the category is any of: 'Throttled', 'ConnectionTerminated' then it is a soft bounce.
4. Everything else is not handled. They are server errors and we should not take any action.
 
Here's the thing, i can understand that : 'BlackListed', 'NoMailbox', 'AccountProblem' should be hard bounces, make sense.
But to mark a subscriber as soft bounce because of: 'GreyListed', 'DNSProblem', 'WhitelistingProblem', 'CodeError', 'ManualCancel' etc doesn't make any sense. These are server errors, they have nothing to do with the subscriber itself, so we need to change the logic here.
As per EE, only "AccountProblem" and "NoMailbox" should be hard bounces.
Here, the thing to be noted is, an email is NOT DELIVERED in the above cases and we don't have absolutely NO WAY @ MW to know if the email was actually delivered or not if those statuses are not handled/soft bounced.
Another note is that SPAM should not be a hard/soft bounce according to it's definition from the link you gave: "The email was rejected because it matched a profile the internet community has labeled as Spam."
We should not mark a subscriber as soft/hard bounce in this case because the subscriber address is fine, the problem is the filter before the email reaches the subscriber. Maybe sending with another delivery server will land the email inbox, we should not punish the subscriber for this. makes sense?
What you told is correct, spam happens when the recipient server rejects an email due to the content/spamminess. But the thing is that, email was not delivered. It should be soft bounced to indicate the email wasn't delivered. And in the case of SPAM, subscribers are unsubscribed as per the current logic - this contradicts your statement as well - "we should not punish the subscriber for this".

Now, given the above, here's the current logic:
1. When the category is Unknown and the status is AbuseReport, then this is clearly a unsubscribe action in mailwizz.
2. When the category is any of: 'BlackListed', 'NoMailbox', 'AccountProblem' then it is a hard bounce.
3. When the category is any of: 'Throttled', 'ConnectionTerminated' then it is a soft bounce.
4. Everything else is not handled. They are server errors and we should not take any action.
#1 - correct (am checking with them what should be the category when status is AbuseReport)
#2 - When it's NoMailbox, or Account problem - it should be hard bounce. For BlackListed, it should be soft bounce - because, this should be due to the poor reputation of IP, if we change the server, it should go through. If we mark this as hard bounces, no further attempts will be done.
#3 - true (and should add other statuses as well)
#4 - We can disregard the manual cancel category here. All others should soft bounce the email. Because, we don't have a way to understand if emails falling in to other categories are actually delivered or not - obviously they're not delivered, and this should be captured by MW.
 
I just got reply from EE support -

From: Cully (Support staff)

Hi,
There is no Category value for Abuse Report -
The Category will only have a value if the status is Bounced.

Abuse Report means that a recipient has marked an email as SPAM/Junk

Cully

So,

#1 - There shouldn't be an AND operator to check if the category is "Unknown". Looks like there won't be any value for "category" parameter in the webhook if "status=AbuseReport". So, if "status=AbuseReport", it should be unsubscribed without any further checks. It's uncertain that "category" will be equal to "Unknown" in such cases.
 
Hey man,

First of all, i thing EE really mess up things, for such a huge company, their docs lack big time and they also don't explain things properly.
Now that we got that out,
What you told is correct, spam happens when the recipient server rejects an email due to the content/spamminess. But the thing is that, email was not delivered. It should be soft bounced to indicate the email wasn't delivered. And in the case of SPAM, subscribers are unsubscribed as per the current logic - this contradicts your statement as well - "we should not punish the subscriber for this"
Soft bounces happen when the receiver rejects the message for a reason or another. This isn't the case(see below).

#2 - When it's NoMailbox, or Account problem - it should be hard bounce. For BlackListed, it should be soft bounce - because, this should be due to the poor reputation of IP, if we change the server, it should go through. If we mark this as hard bounces, no further attempts will be done.
They give absolutely no detail about when this happens exactly, if it's like you say, then again, it should not be a bounce of any type because the problem is because of the server, has nothing to do with the subscriber.

Seems we're not on the same page about what a soft bounce actually means.
If the email reaches the end subscriber box and for whatever reason that subscriber does not accept the email(excluding spam), i.e: email full, subscriber in vacation, etc, then that is a soft bounce.
If the email returns because the server was not able to make the delivery because of it's own fault(dns issue, low reputation, etc) then that's not a soft bounce, it's just a server issue, which, in the EE case will be retried again for sending and if it fails, it will eventually return with a hard bounce category.

However, because you need other workflow than what the defaut mailwizz one will be, it doesn't mean you cannot implement it for your own needs.
From next release, you will be able to replace the elasticemail bounce processing method very easily, from and extension:
PHP:
public function run() {

    Yii::app()->hooks->addFilter('dswh_process_map', function($map, $server, $controller){

        $map['elasticemail-web-api'] = function($server, $controller) {
       
            $request     = Yii::app()->request;
            $category    = trim($request->getQuery('category'));
            $transaction = trim($request->getQuery('transaction'));
            $status      = trim($request->getQuery('status'));

            if (empty($transaction) || empty($category)) {
                Yii::app()->end();
            }

            $deliveryLog = CampaignDeliveryLog::model()->findByAttributes(array(
                'email_message_id' => $transaction,
                'status'           => CampaignDeliveryLog::STATUS_SUCCESS,
            ));

            if (empty($deliveryLog)) {
                $deliveryLog = CampaignDeliveryLogArchive::model()->findByAttributes(array(
                    'email_message_id' => $transaction,
                    'status'           => CampaignDeliveryLogArchive::STATUS_SUCCESS,
                ));
            }

            if (empty($deliveryLog)) {
                Yii::app()->end();
            }

            $campaign = Campaign::model()->findByPk($deliveryLog->campaign_id);
            if (empty($campaign)) {
                Yii::app()->end();
            }

            $subscriber = ListSubscriber::model()->findByAttributes(array(
                'list_id'          => $campaign->list_id,
                'subscriber_id'    => $deliveryLog->subscriber_id,
                'status'           => ListSubscriber::STATUS_CONFIRMED,
            ));

            if (empty($subscriber)) {
                Yii::app()->end();
            }

            $bounceLog = CampaignBounceLog::model()->findByAttributes(array(
                'campaign_id'   => $campaign->campaign_id,
                'subscriber_id' => $subscriber->subscriber_id,
            ));

            if (!empty($bounceLog)) {
                Yii::app()->end();
            }
           
            // All categories:
            // https://elasticemail.com/support/delivery/http-web-notification
           
            if ($status == 'AbuseReport') {
                if (Yii::app()->options->get('system.cron.process_feedback_loop_servers.subscriber_action', 'unsubscribe') == 'delete') {
                    $subscriber->delete();
                    Yii::app()->end();
                }

                $subscriber->setStatus(ListSubscriber::STATUS_UNSUBSCRIBED);

                $trackUnsubscribe = CampaignTrackUnsubscribe::model()->findByAttributes(array(
                    'campaign_id'   => $campaign->campaign_id,
                    'subscriber_id' => $subscriber->subscriber_id,
                ));

                if (!empty($trackUnsubscribe)) {
                    Yii::app()->end();
                }
               
                $trackUnsubscribe = new CampaignTrackUnsubscribe();
                $trackUnsubscribe->campaign_id   = $campaign->campaign_id;
                $trackUnsubscribe->subscriber_id = $subscriber->subscriber_id;
                $trackUnsubscribe->note          = 'Unsubscribed via Web Hook!';
                $trackUnsubscribe->save(false);

                Yii::app()->end();
            }
           
            $bounceCategories = array(
                'NoMailbox', 'AccountProblem',
                'Throttled', 'ConnectionTerminated',
            );
            $bounceCategories = array_map('strtolower', $bounceCategories);
            $categoryID       = strtolower($category);

            if (in_array($categoryID, $bounceCategories)) {
                $hardBounceCategories = array('NoMailbox', 'AccountProblem');
                $hardBounceCategories = array_map('strtolower', $hardBounceCategories);
               
                $softBounceCategories = array('Throttled', 'ConnectionTerminated');
                $softBounceCategories = array_map('strtolower', $softBounceCategories);

                $bounceType = null;
               
                if (in_array($categoryID, $hardBounceCategories)) {
                    $bounceType = CampaignBounceLog::BOUNCE_HARD;
                } elseif (in_array($categoryID, $softBounceCategories)) {
                    $bounceType = CampaignBounceLog::BOUNCE_SOFT;
                }
               
                if ($bounceType === null) {
                    Yii::app()->end();
                }

                $bounceLog = new CampaignBounceLog();
                $bounceLog->campaign_id     = $campaign->campaign_id;
                $bounceLog->subscriber_id   = $subscriber->subscriber_id;
                $bounceLog->message         = $category;
                $bounceLog->bounce_type     = $bounceType;
                $bounceLog->save();

                if ($bounceLog->bounce_type == CampaignBounceLog::BOUNCE_HARD) {
                    $subscriber->addToBlacklist($bounceLog->message);
                }
               
                Yii::app()->end();
            }

            Yii::app()->end();
        };

        return $map;
    });

}
So you'll be able to easily implement your own logic, different than what the default one is, which means this way is also good for you but also for mailwizz.
 
  • Like
Reactions: VVT
You may be right in the aspect of distinguishing between soft and hard bounces, but since I rely on EE alone, I should somehow make MW track the bounces. And I agree that EE documentation sucks, and does their support !

So, the code snippet above is for next release, right ?

Can you please confirm if modifying the dswh file like I did above will work so that I will have a temporary work around ?
 
All of the above reinforces my impression, that these categories are generally not only not well-defined, (especially AccountProblem seems to include a wide spectrum of errors, with hundreds of different error messages across the web having interesting/funny admin touches to them) but they are also badly implemented across many mail servers (and sometimes purposefully misleading), and hence the messages coming back are all over the place with their error indications, which makes it very hard to deal decisively with them.

Consequently, it would be good to have a clear/small core for hard bounces, and
(as far as possible avoiding further sending/reputational problems) have flexibility
in handling the large category of all else (soft bounces and other error messages).

So, @twisted1919 if you could make the bounce category very small/clear from the outset,
and leave the rest for adjustment (though with wise default settings), that would be good.
It seems this was largely, and is now even more so the case.

thx :)
 
Last edited:
@VVT - Don't modify the dswh file after you update. Instead, create an extension (do you want me to do it for you? is too simple) maybe using the example extension and in it place the code i gave you above and do changes in that code. Basically the above code is copied from the upcoming release and once you paste it in your own extension and hook in the dswh_process_map it will override the default one from mailwizz.
 
That's cool, I'll try to get it done myself and reach out to you if I can't ! Thank you :)
 
So is this fixed in v1.3.6.5? My setup with Elastic SMTP/API is not getting the bounces. In order for MW to get click stats I must enable '
'Notify On: Clicked' in elastic. Same thing with unsubscribes. If 'Notify On: Unsubscribed is not on in Elastic then unsubscribes doesnt work in my MW nevermind just getting the stats.
 
So is this fixed in v1.3.6.5? My setup with Elastic SMTP/API is not getting the bounces. In order for MW to get click stats I must enable '
'Notify On: Clicked' in elastic. Same thing with unsubscribes. If 'Notify On: Unsubscribed is not on in Elastic then unsubscribes doesnt work in my MW nevermind just getting the stats.

update to 1.3.5.9, as many things have been improved
 
Well that did the trick. Thank you. lol I dont know why I thought 1.3.6.5 was the latest version.
 
Well that did the trick. Thank you. lol I dont know why I thought 1.3.6.5 was the latest version.
Did it not show that an update was available in the app (where otherwise error messages appear)?
You can set it in the backend ;)
 
Last edited:
Back
Top