Set up a Weekly (weekend) autoresponder

Years ago I set up an autoresponder to let people know I’m probably not around on Saturday and Sunday. It’s still running, and flawlessly, so I thought I’d share how I set it up using cPanel cron and FTP/File Manager. I got this idea originally from David Heath-Whyte’s blog, but adjusted it a little bit. It might help you to see two examples if you want to set this up yourself and aren’t quite sure how. Sometimes having the same thing explained twice by two different people helps!

Note: this advice only works for people with self-hosted email. Gmail, Hotmail, Yahoo! etc., look elsewhere. Also, you’ll need hosting with access to a cPanel.

Step 1: Set up and compose your Autoresponder email

Head over to your cPanel (“control panel”). Most hosting accounts will have a control panel of some sort, and cPanel is probably the most ubiquitous type. Look for “Autoresponders” in the Email section, and click that.

When the Autoresponders page has loaded, click “Add Autoresponder.” You can only have one autoresponder for an email address, so if you already have one set up for the email address in question, just review its settings.

Below are mock settings for my autoresponder. I set the interval pretty high at 16 hours. What this means is that if someone sends me an email every hour over the weekend, they won’t get the autoresponder again until they’ve send the seventeenth email. Receiving an auto-responder every time when we already know someone is out of the office could be annoyingly inbox-stuffing. Beyond that it could create problems (such as loops) if an autoresponder replies to my autoresponder.

I have set the autoresponder to begin at 6pm this coming Thursday because in my dreams I’m taking long weekends, but you might want to switch that to something like 5pm on Friday. I stretch the weekend into Sunday evening, but stop there because most people emailing Sunday night understand that they’re not going to get a reply until Monday anyway. Those late Sunday e-mailers don’t need an auto-responder. We’d just be rubbing it in that they’re working and we aren’t!

Another common approach is to make the “From:” address a blackhole email address (e.g. no-reply@sagehen.studio or autoresponder@sagehen.studio) and indicate to people in the message that the message comes from a bot and replies will be delivered into the far reaches of another galaxy. This can help prevent autoreply loops from cluttering your inbox. Blackhole email addresses can be set up using cPanel, but that’s a whole other thing.

Step 2: Locate the corresponding autoresponder.json file

When we edit an autoresponder in cPanel, the data is saved in a JSON-formatted file. These are found in the root server folder “.autoresponder”. I want to know the absolute path (ABSPATH) of the file named email@domain.com.json. I use Transmit for FTP and it allows me the handy feature “Copy URL.” Your application probably offers something similar.

“Copy URL” gives a URL, not an ABSPATH. I know how to convert that in my head, since I’m familiar with my server. If you have no idea how to get the absolute path from a URL, then you’re in a little over your head. But no worries, a call to your web hosting company could sort it out, or good old Google could help you. In my case the absolute path to this file looks like:

/home/galuxee/.autorespond/caroline@sagehen.studio.json

Note your ABSPATH, we’ll be using it soon.

While we are in FTP, or browsing files using SSH or File Manager, let’s create a folder in our public_html/ folder called “cron”. That’s a nice way of keeping things organized, since ultimately we are creating a CRON job.

The following PHP script is what our CRON job will run each week. Each week when CRON parses this file, it will be told to move the weekend pointer forward one week, and the JSON file will be edited accordingly. Bottom line: this script is automating the process of logging into cPanel every week and editing the dates manually.

<?php

/** 
 * Set the time zone so our weekend basis is correct
 * Do not do this inside a WordPress installation
 */
date_default_timezone_set( 'America/Los_Angeles' ); // EDIT

/**
 * Set the start/end times.
 * strtotime() is a PHP function that is pretty liberal in what it will accept as a string
 * It then converts the string to a UNIX timestamp (a string of numbers equal to the number
 * of seconds since Unix Epoch on January 1st, 1970 at UTC
 */
$nextFriEve = strtotime( "next friday 18:00:00" );
$nextSunEve = strtotime( "next sunday 18:00:00" );

// Make the array which populates a healthy .autoresponder JSON file:
$auto_responder = array(
    'start' 	=> $nextFriEve,
    'interval'    => 57600, // 16 hours
    'stop' 		  => $nextSunEve,
);

/**
 * Now save the encoded array to the .json file
 * Here is where we use the ABSPATH 
 */
@file_put_contents( '/home/galuxee/.autorespond/email@domain.com.json', json_encode( $auto_responder ) ); // EDIT

/**
 * Confirmation email
 * Let's send ourselves a reassuring weekly email
 * It will confirm the autoresponder has been set up, and for what time span
 * CRON will echo this in its own confirmation email to YOU when it completes, as
 * long as you have a CRON email set up in cPanel. That's shown in the next step.
 */
echo "Howdy,\n\n";
echo "Just a reminder... your email@domain.com weekend autoresponder file was re-written for the week:\n\n";
echo "It starts on " . date( "F j, Y, g:i a", $nextFriEve );
echo "\n\nAnd ends on " . date( "F j, Y, g:i a", $nextSunEve );
echo "\n\nHave a fabulous weekend!";
echo "\n\nGenerated for you by https://domain.com/cron/autoresponder_weekend.php";

In order for this to work you at least have to edit the lines commented with “// EDIT”. Review and personalize everything else, such as the time zone, dates, interval, and the confirmation email.

Save this file as “autoresponder_weekend.php” inside your “public_html/cron/” folder. We’re almost there. Just one more step before the weekend…

Step 3: Create a CRON job using cPanel

If you’re building a website or managing email with cPanel, it’s worthwhile to get familiar with its features. Obviously. Cuz look what we’re doing now! We’re practically ruling the universe! What I’m trying to say is cPanel is a very powerful tool. As is CRON. Together they can create a “smart home” out of your hosting account. Use them wisely.

We are going to set up a CRON job. If you already have CRON jobs, you’ll want to set this one up to run at a different time than the others, esp. if the others are heavy processes. Mine is set up to run two minutes after my other CRON job, and away from when I run backups on my server (in the middle of the night), just to give the processes a little space. ???? You don’t have to do this. I just try to be nice to my computer since it’s doing me this big favor of running automatic auto-responder updates for me every week.

In the image above you can see that I’ve got an email set up for the autoresponder. If you do not want a confirmation email for this particular CRON job you can use the “/dev/nul 2>&1” command:

/user/bin/php -q /home/galuxee/cron/weekend_autoresponder.php > /dev/null 2>&1

I like getting the weekly emails as they get me pumped for the weekend. My CRON job runs at 14:02 every Thursday afternoon. The weekend is coming!

The command points to the PHP script we wrote and saved earlier.

Depending where the PHP library is kept on your server, your command might look like:

php /home/galuxee/domain/cron/weekend_autoresponder.php

or even

It sort of just depends on what you are doing. The “-q” command assumes that your PHP file is solid and skips any output generated by faults, so I use it, since the code is so simple. If you are having troubles you might want to omit that and check your debug logs.

If you can’t wait to test this setup, you can speed things up. Change the CRON job to run every few minutes, then open up your .autoresponder JSON file to see if the weekend dates have changed (in UNIX format; here’s a handy UNIX time converter). You can corroborate the change in your cPanel autoresponder settings.

Happy Weekend!

Awesome. I hope this helps you put aside work and have a relaxed weekend, every weekend. Maybe it also helped you learn a little more about cPanel and CRON jobs, too.

And finally: thanks, David!

Fix Easy Digital Downloads to Increase Sales

Over the years I’ve sure appreciated having Easy Digital Downloads (EDD) plugin to platform my plugin sales. It’s pretty cool, and handles most my needs including software licensing.

However, I have had numerous little issues with the plugin. I apply each update with trepidation, because often changes (“upgrades”) cause abrupt slumps in my sales. Of course the developers are well-meaning, and I’m sure these upgrades are in fact upgrades for certain people. But it’s becoming more and more clear that even though I’m a paying subscriber of EDD, their developers might not be “in touch” with the my reality or my customers’ realities.

There are things a customer just. does. not. want. to. see. when. shopping.

Case in Point

With the latest updates to the Stripe payment gateway plugin, EDD moved to the new API and allows small text to appear on screen just below the credit card fields, which easily would scare off a good number of customers:

“By providing your card information, you allow [Business Name] to charge your card for future payments in accordance with their terms.”

Oh dear God! How long has my website been telling people THAT? “Their terms?” What terms? Where does the customer now go to find those terms? Away from your checkout, that’s where.

Let’s Fix THAT

I know that EDD is stressing the subscription model and has a strong integration with Stripe for that model, but for those of us running EDD shops without subscription, this kind of small print is… not great. Luckily there’s an easy fix, but it does involve a few lines of PHP

add_filter( 'edds_stripe_payment_elements_terms', function( $terms ) {
    $terms['card'] = 'never';
    $terms['applePay'] = 'never';
    $terms['sofort'] = 'never';
    $terms['cashapp'] = 'never';
    $terms['bancontact'] = 'never';
    // view more methods here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms 
	return $terms;
} );

Other payment methods might need to have these off-putting terms turned off too. You can view their keys here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms

My Verdict

The card terms should be set to “never” by default, and set to the other ☠️☠️☠️ option when EDD store subscriptions are enabled. Developers should be serving the majority of their users, esp. their paying users. I can’t imagine that most EDD users are using the subscription model. It’s expensive, complex and controversial amongst merchants and customers.

After various other SNAFUs over the years, makes me very hesitant to apply site upgrades on anything but a development server. This is frustrating. I just don’t like the prospect of combing through upgrades to review UX each time. I’m paying them so it just doesn’t seem like it should be my job. </tirade>

Bridging the Gap between PHP 5.3 and 8.2

This week I got finally got involved with maintaining the languishing TCPDF library on Github. Long overdue. Embarrassingly long overdue, since I’ve based one of my businesses on this open source gem for nearly ten years now.

The author, Nicola Asuni, has been working for many years on a successor to TCPDF, but it is stalled… likely due to the usual reasons: life and cash. I can relate. I’ve been sitting on a huge WP plugin project for two years now, desperately trying to move it forward while life happens and I’m forced to prioritize support for my paying customers.

A couple years ago an eager young programmer, William Desportes, hopped onto the TCPDF GitHub and made an incredible fork which would have really helped bring TCPDF into this decade. It needed massaging to work with PHP 8.0+ and he gave it a rub. However, understandably, the original author reminded Desportes he was already working on a new version, and that helping with/watching the fork would just be too much. Probably annoying, too.

Asuni invited Desportes to be a project contributor on the old TCPDF. Since, Desportes has been the most active, if not the only active contributor to TCPDF, with Asuni popping in to approving PRs. And largely thanks to Desportes, the library is keeping up with huge PHP updates.

Anyway, in my testing I discovered a bug in TCPDF while testing with PHP 8.2:

Warning: use of “self” in callables is deprecated

Uh oh. This means we cannot use self::method as callables inside functions that call them, like array_map(), any longer in PHP 9.0. We also cannot do a thing where we name the class and method in an array, e.g. array('class', 'method')

We have a little bit of time to fix this before PHP 9.0 comes out, but everyone hates a warning so best fix it now. An instant replacement for using self::method would look like:

self::class . "::method"

Another option could be something like:

[self::class, "method"]

Neither of these will work with PHP 5.3. The moment PHP 5.3 sees ::class, it breaks. The moment PHP 5.3 and 5.4 see square brackes, they break.

So how the hell do we get our fix to work with both PHP 8.2 AND PHP 5.3 (TCPDF is stated compatible down to PHP 5.3)?

Issue: https://github.com/tecnickcom/TCPDF/issues/632

Corresponding Pull Request: https://github.com/tecnickcom/TCPDF/pull/633

I was pretty excited to patch this up quickly, but my initial fix was no good. Somehow I got lost in testing and was led to believe it worked in PHP 5.3 But ::class isn’t a thing until PHP 5.5. Back to the drawing board! I do not and cannot use a test for PHP version because:

  • Moving the code for newer versions to newly created files is cumbersome and feels backward-moving
  • Again, the instant PHP 5.3 sees code it doesn’t recognize, e.g. ::class or [] notation, it just breaks

So I spend some time testing various things, trying to find a reference to the static method that both PHP 5.3 and PHP 8.2 will use unbegrudgingly. And after trying various methods and going back and forth between 5.3 and 8.2 and all versions in between, I find that using `get_called_class` works as a replacement for array(‘TCPDF_FONTS’, ‘unichrUnicode’), especially these static methods I’m looking to resolve in TCPDF. Thus

return array_map(array('TCPDF_FONTS', 'unichrUnicode'), $ta);

becomes:

 return array_map(get_called_class() . '::unichrUnicode', $ta );

I’ve sorted it out, but in the process I sorta lose my marbles and start posting about nuts on Github. I’m battling the frustration of WHY ARE WE STILL coding for PHP 5.3 when it was deprecated 9 years ago?! I’m also battling some imposter syndrome. Like, why am I the chosen one today? I hardly know how to use Github! How can I be one of the only people left working on this massively popular library? Will this work for everyone the way it worked for me in test after test, or will this be my “Carrie at the prom” moment, having finally just stuck out my neck on Github? Time will tell! But for now, it works, so… you’re welcome. ☺️

Easy Digital Download Confusion

Once a customer has purchased more than one download from your Easy Digital Downloads (EDD) e-commerce shop, and especially if they have renewed their purchases year-over-year, there will be a source of confusion for them in their account pages, if/when they go to fetch a download file.

The list of downloads will include duplicate links in table rows, and the customer might not know which link to click to get the latest file. In fact, all the links with the same title are the same download. But how are they to know that? Why should they have to suss all that out, when it should just be a quick grab-n-go?

I get asked about this repeatedly, so I decided to code in a fix. I initially thought I’d solve this problem using the ‘edd_before_download_history‘ action hook in the ‘easy-digital-downloads/templates/history-downloads‘ template file, but that action hook doesn’t include the $orders variable, and that’s a shame because I can’t conditionally show a message if the customer has more than one download, e.g.

if ( count( $orders ) > 1 ) {
    // Then show an awkward message, like:
    echo '

awkward message explaining how all the links below with the same title are actually to the same file.

'; }

I’d have to put my awkward message in the template if I wanted to be at least minimally awkward about my message (I mean, only showing it to customers with more than one order). Sigh.

And so I ended up uploading a template override after all, which I try to avoid because updating templates every time EDD updates the parent templates is a pain. That said, I’ve updated EDD and WooCommerce templates so many hundreds of times I could almost do it with my eyes closed. That doesn’t mean it’s fun. I wish EDD would include some $variables in more of its action hooks.

Here’s what I did. I added some logic such that the $orders variable contains orders by DATE, in descending (DESC) order. That way, when the orders are looped through in the template, as they already are, I can collect item product IDs in an array. If that item product ID is already shown, I won’t show it again:

First, add two lines after line 31, to add to the edd_get_order query args:

'type'           => 'sale',
'status__not_in' => array( 'trash', 'refunded', 'abandoned' ),
'order'          => 'DESC', // add this line
'orderby'        => 'date_created', // add this line, too!

Next, add one line after line 51, so it looks more like this:

$prod_ids = [];
foreach ( $orders as $order ) :

Finally, add 6 lines of code after line 53, so it looks like this:

foreach ( $order->get_items_with_bundles() as $key => $item ) :

    if ( in_array( $item->product_id, $prod_ids ) ) {
        continue; // download link is already shown
    }
    if ( $item->is_deliverable() ) {
        $prod_ids[] = $item->product_id;
    }

Makes it much easier for a customer to just get their download without guesswork, which is what EDD is all about, right?

Prorated Software Upgrade Crack

It’s amazing how some customers’ behavior can highlight innovative ways of saving money.

It never occurred to me until I started selling software, and managing annual software licenses, that there’s a pretty simple way to save money when trying to renew software licenses, without a coupon. Many businesses pro-rate license upgrades, and prorate them by date, such that the closer you come to expiration, the cheaper an upgrade gets. So a frugal move to make is to wait until just days before your software license expires, then upgrade to the next level up… for a tiny fraction of what it would cost to renew the lesser license currently held. Voilà – we now have better access for another year, for cheaper than a straight renewal!

While this is pretty cool for the customer, this is a big problem for software sellers that causes significant loss of income. In particular it’s a weakness for some Easy Digital Downloads (EDD) admins using EDD’s Software Licensing extension. I wrote a PHP code snippet for use with EDD Software Licensing. It locks out prorated upgrades after a certain number of months of license ownership (3 months):

/**
 * Customers lose prorate upgrades if less than 9 months left on license
 *
 * @param float|int $prorated Calculated prorated price
 * @param int $license_id ID of license being upgraded
 * @param float|int $old_price Price of the original license being upgraded
 * @param float|int $new_price Price of the new license level
 * @return float|int
 */
function my_get_pro_rated_upgrade_cost( $prorated, $license_id, $old_price, $new_price ) {

    if ( ! class_exists( 'EDD_Software_Licensing' ) ) {
        require_once WP_PLUGIN_DIR . '/edd-software-licensing/includes/classes/class-edd-software-licensing.php';
    }
    // Get license object from license ID
    $license = EDD_Software_Licensing()->get_license( $license_id );
    if ( 'lifetime' === $license->expiration ) {
        return $prorated; // why lifetime licenses should get caught in this hook is beyond me
    }
    // 23670000 is the number of seconds in 9 months, change ad lib
    if ( ! is_object( $license ) || $license->is_expired() || ( ! $license->is_expired() && ( (int) $license->expiration - time() < 23670000 ) ) ) {
        return $new_price; 
    }
    return $prorated;

}
add_filter( 'edd_sl_get_pro_rated_upgrade_cost', 'my_get_pro_rated_upgrade_cost', 11, 4 );

Should a customer decide they need to upgrade the software, they need to do it soon after purchase, not as a way to obtain a deep discount. In the case of this EDD add-on snippet below, they need to upgrade within 3 months, otherwise prorating is not offered. If they try to upgrade an expired license, prorating is not offered.

I hope this helps you. If it does please reach out to say hi!

An Easy Digital Download Software Licensing Server Attack

If you’ve purchased the Easy Digital Downloads Software Licensing add-on (min price $99/yr) and you have your eyes on API server traffic, you might have noticed that if your site runs CRON jobs as frequently as every minute, you are potentially getting hit by thousands of HTTP requests every hour (depending of course on how many license keys you’ve issued to customers). This can lead to DB errors and worse. That’s just not cool for the price.

EDD provides some “free” example integration code to get users started. Great, but we really shouldn’t use any volunteered code without an audit. Here’s the code they’ve provided:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

	// To support auto-updates, this needs to run during the wp_version_check cron job for privileged users.
	$doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;
	if ( ! current_user_can( 'manage_options' ) && ! $doing_cron ) {
		return;
	}

	// retrieve our license key from the DB
	$license_key = trim( get_option( 'edd_sample_license_key' ) );

	// setup the updater
	$edd_updater = new EDD_SL_Plugin_Updater(
		EDD_SAMPLE_STORE_URL,
		__FILE__,
		array(
			'version' => '1.0',                    // current version number
			'license' => $license_key,             // license key (used get_option above to retrieve from DB)
			'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
			'author'  => 'Easy Digital Downloads', // author of this plugin
			'beta'    => false,
		)
	);

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Lines 7-11 of this PHP code indicate that as long as a site administrator is logged into the site and loads any page, or every time a CRON job runs, fire the EDD_SL_Plugin_Updater() class, which runs calls to the server running the API. Some CRON jobs run every minute, plus there are all the other cron jobs. Some admin users can load A LOT OF PAGES every minute, too.

YOIKS!

I added some checks to their suggested code so that even though the function runs with every page load (on the ‘init’ hook), it only ends up running if:

  • CRON is not running yet an administrator is viewing a plugin update screen
  • CRON is running and WP auto updates are turned on for the plugin

If auto updates are turned on this could still mean a lot of requests, but this certainly cuts way down on pointless API calls. Transients could also certainly be set; some plugins rate limit that way.

Slightly Optimized Version:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

    if ( wp_installing() ) {
        return;
    }

    $doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;

    if ( $doing_cron ) {
        /** 
         * If running cron, only continue if plugin auto updates are on
         * (The 'wp_version_check' CRON job supports auto-updates)
         */ 
        if ( 'yes' !== get_option( 'my_plugin_auto_updates', 'yes' ) ) { // if your plugin doesn't have a setting for this you can probably fetch the setting using hooks
            return;
        } else {
            // Probably wise to get a transient right here to slow this down...
        }
    } else {
        // If not doing cron, only check for updates for admin users
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        global $pagenow;
        // Cut back on requests by only continuing if on plugin updates page
        if ( isset( $pagenow ) && 'plugins.php' !== $pagenow && 'update-core.php' !== $pagenow ) {
            return;
        }
    }
    // retrieve our license key from the DB
    $license_key = trim( get_option( 'edd_sample_license_key' ) );

    // setup the updater
    $edd_updater = new EDD_SL_Plugin_Updater(
        EDD_SAMPLE_STORE_URL,
        __FILE__,
        array(
            'version' => '1.0',                    // current version number
            'license' => $license_key,             // license key (used get_option above to retrieve from DB)
            'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
            'author'  => 'Easy Digital Downloads', // author of this plugin
            'beta'    => false,
        )
    );

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Get in touch if you need help with this or you have better ideas!