23. Install phpList

by Double Bastion - Updated February 19, 2022

Unlike Mailman which is used by list subscribers to send emails to the list’s email address, so that they can be automatically sent to all the subscribers of that list, phpList is used by the list owner to send one email (usually a newsletter, or marketing email) directly to a large number of subscribers, and if any subscriber responds, the response gets sent to the ‘Reply-To’ address, configured by the list owner.

In phpList, the subscription process, the unsubscribe process, the bounces management are automated. Subscriptions to one or more lists are made through a subscription page that can be integrated into a website. phpList also has tracking capabilities, offering information about the number of subscribers who opened the email and about the number of links inside the emails, that they followed.

First create the new directory for phpList:

cd /var/www

mkdir lists.example.com

To obtain a Let’s Encrypt SSL certificate for lists.example.com, first edit the Nginx server blocks configuration file:

nano /etc/nginx/sites-enabled/0-conf

Add the following temporary server block for lists.example.com at the end of the file:

server {

listen 80;

listen [::]:80;

server_name lists.example.com;

location /.well-known/acme-challenge {

root /var/www;

}

}

Replace example.com with the main domain hosted on your server. Restart Nginx:

systemctl restart nginx

Edit your DNS settings. Add an A entry and an AAAA entry for lists.example.com. These entries will be similar to the entries you already have for mail.example.com. It’s just that instead of mail, you will enter lists . Wait a few minutes until the DNS changes propagate.

Get the Let’s Encrypt SSL certificate for lists.example.com:

certbot certonly –agree-tos –webroot -w /var/www/ -d lists.example.com

To install phpList first access the official download page in a browser: https://www.phplist.org/download-phplist/ . There right-click on the ‘Download ZIP’ link and choose ‘Copy link location’, to copy the URL of the downloadable package to the clipboard. Then, on your server run:

cd /tmp

wget https://sourceforge.net/projects/phplist/files/phplist/3.5.6/phplist-3.5.6.zip/download

where 3.5.6 is the current version of phpList. Next uncompress the archive and copy the lists directory to /var/www/lists.example.com:

unzip download

cp -r /tmp/phplist-3.5.6/public_html/lists /var/www/lists.example.com

Remove the files and folders that are not needed anymore:

cd /tmp

rm -r phplist-3.5.6

rm download

cd /var/www/lists.example.com/lists

rm .htaccess

Change the ownership and permissions for the /var/www/lists.example.com directory and all its subdirectories:

chown -R www-data:www-data /var/www/lists.example.com

find /var/www/lists.example.com -type d -exec chmod 750 {} +

find /var/www/lists.example.com -type f -exec chmod 640 {} +

The next step is to log in to phpMyAdmin and create a database and a user for phpList. Name the database phplistdb and the user phplist. Give the user all the privileges over the phplistdb database, except GRANT, which is not necessary. Write down the password of the phplist user because you’ll need it.

Then edit the /var/www/lists.example.com/lists/config/config.php file:

nano /var/www/lists.example.com/lists/config/config.php

Edit the following settings, to make them look like this:

$database_host = ‘localhost’;

$database_name = ‘phplistdb’;

$database_user = ‘phplist’;

$database_password = ‘yourstrongpassword‘;

define(‘PHPMAILERHOST’,’mail.example.com‘);

define(‘PHPMAILERPORT’,’587′);

define(‘PHPMAILER_SECURE’,’tls’);

$phpmailer_smtpuser = ‘bounces@example.com‘;

$phpmailer_smtppassword = ‘bouncespassword‘;

define(‘TEST’, 0);

define(‘USE_REPLY_TO’, 1);

$message_envelope = ‘bounces@example.com‘;

$bounce_protocol = ‘pop’;

define(‘MANUALLY_PROCESS_BOUNCES’, 0);

define(‘USE_ADVANCED_BOUNCEHANDLING’, 1);

$bounce_mailbox_host = ‘mail.example.com‘;

$bounce_mailbox_user = ‘bounces@example.com‘;

$bounce_mailbox_password = ‘bouncespassword‘;

//$bounce_mailbox_port = ‘110/pop3/notls’;

$bounce_mailbox_port = “995/pop3/ssl/novalidate-cert”;

//$bounce_mailbox = ‘/var/mail/listbounces’;

$bounce_unsubscribe_threshold = 4;

define(‘UPLOADIMAGES_DIR’, ‘lists/images’);

// USE_MANUAL_TEXT_PART creates a ‘Text’ tab with a text area for the plain text version of the message

define(‘USE_MANUAL_TEXT_PART’,1);

define(‘SESSION_TIMEOUT’, 28800);

define(‘REGISTER’, 0);

define(‘EMAILTEXTCREDITS’, 1);

define(‘PAGETEXTCREDITS’, 1);

define(‘NOSTATSCOLLECTION’, 1);

Replace example.com with the main domain hosted on your server and yourstrongpassword and bouncespassword with the real passwords.

The SESSION_TIMEOUT parameter sets the time interval after which the logged in user is logged out automatically. You can set it to 28800 seconds (8 hours), or you can reduce/increase that value.

These four lines are optional:

define(‘REGISTER’, 0); – disables requesting the Powered By image from www.phplist.com.

define(‘EMAILTEXTCREDITS’, 1); – removes the Powered By image from the emails’ footer

define(‘PAGETEXTCREDITS’, 1); – removes the Powered By image from the public web pages (subscribe page, preferences page, forward page, etc.).

define(‘NOSTATSCOLLECTION’, 1); – disables sending statistics to phplist-stats@phplist.com

Please note that in the $phpmailer_smtpuser, $message_envelope and in the $bounce_mailbox_user variables you have to have the same email address, otherwise the emails won’t be sent. This is because the /etc/postfix/main.cf file contains the reject_sender_login_mismatch directive which is really necessary, because it ensures that the users logged in to Postfix use their real email address in the ‘Return-Path’ and ‘envelopefrom’ headers of the emails they send. Since bounces@example.com is necessary to process bounced emails and it has to be present in the ‘Return-Path’ and ‘envelopefrom’ headers of sent emails, phpList should use it to log in to the SMTP server as well, so it has to be mentioned in the $phpmailer_smtpuser, $message_envelope and in the $bounce_mailbox_user variables.

23.1. Remove the ‘Powered By’ text link from the footer of emails and public pages

If you removed the ‘Powered By’ image from the footer of phpList’s outgoing emails and public pages (subscribe, unsubscribe, ‘update your preferences’ pages), by including define(‘EMAILTEXTCREDITS’, 1); and define(‘PAGETEXTCREDITS’, 1); in the configuration file, you will notice that the image has been replaced with a ‘Powered By’ text link. Unfortunately, the configuration file doesn’t offer an option to also remove the text link. Since phpList is licensed under the GNU Affero General Public License, every user has the right to change the code to make the program function as they need. As a program user, you have the right to modify it in order to remove the ‘Powered By’ text link, if you don’t want this link to appear in the footer of outgoing emails and phpList public pages. To achieve this, open the /var/www/lists.example.com/lists/admin/connect.php file:

nano /var/www/lists.example.com/lists/admin/connect.php

Comment out the $PoweredByText line, to make it look like this:

//$PoweredByText = ‘<div style=”clear: both; font-family: arial, verdana, sans-serif; font-size: 8px; font-variant: small-caps; font-weight: normal; padding: 2px; padding-left:10px;padding-top:20px;”>powered by <a href=”https://www.phplist.com/poweredby?utm_source=download’.$v.’&amp;utm_medium=poweredtxt&amp;utm_campaign=phpList” target=”_blank” title=”powered by phpList version ‘.$v.’, &copy; phpList ltd”>phpList</a></div>’;

Right below it add the following line:

$PoweredByText = ”;

Please note that after you upgrade phpList, you’ll have to make this change again.

We encourage you promote phpList by every means possible and even donate to the project, but you shouldn’t be forced to include the ‘Powered By’ image or text link on any of your public pages or outgoing emails. This is why we explained how to remove them if you want to.

23.2. Move the configuration file outside the web root

To increase security, first copy the /var/www/lists.example.com/lists/config/config.php file outside the web root, to the /srv/scripts directory:

cp /var/www/lists.example.com/lists/config/config.php /srv/scripts/phplist.php

Then empty the /var/www/lists.example.com/lists/config/config.php file:

cat /dev/null > /var/www/lists.example.com/lists/config/config.php

Then open it:

nano /var/www/lists.example.com/lists/config/config.php

Add the following line inside it:

<?php include(‘/srv/scripts/phplist.php’); ?>

Change ownership amd permissions for the /srv/scripts/phplist.php file:

cd /srv/scripts

chown www-data:root phplist.php

chmod 400 phplist.php

23.3. Create the bounces@example.com mailbox

Next, log in to Postfix Admin and create a mailbox called bounces@example.com where phpList will redirect all the bounces (emails that couldn’t be sent because the email address wasn’t valid or for other reasons). After the bounces will be proccessed automatically, once per month for example, all the bounce messages stored in bounces@example.com will be deleted by phpList, because phpList will connect to the mail server through the pop3 protocol, according to the settings from above.

23.4. Configure Nginx for phpList

Then open the /etc/nginx/sites-enabled/0-conf file:

nano /etc/nginx/sites-enabled/0-conf

Replace the temporary server block for lists.example.com added earlier, with the following server blocks:

server {

listen 80;

listen [::]:80;

server_name lists.example.com;

return 301 https://lists.example.com$request_uri;

}

server {

listen 443 ssl http2;

listen [::]:443 ssl http2;

server_name lists.example.com;

root /var/www/lists.example.com;

index index.php;

ssl_certificate /etc/letsencrypt/live/lists.example.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/lists.example.com/privkey.pem;

ssl_trusted_certificate /etc/letsencrypt/live/lists.example.com/chain.pem;

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_session_timeout 4h;

ssl_session_cache shared:SSL:40m;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_prefer_server_ciphers on;

ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

ssl_stapling on;

ssl_stapling_verify on;

add_header Strict-Transport-Security “max-age=63072000” always;

add_header X-Frame-Options SAMEORIGIN;

add_header X-Content-Type-Options nosniff;

add_header X-Robots-Tag “noindex, nofollow, nosnippet, noarchive”;

location = /robots.txt {

allow all;

}

location / {

try_files $uri $uri/ /index.php?$args;

}

location /.well-known/acme-challenge {

root /var/www;

}

location /lists {

charset utf-8;

location ~* \.(txt|log|inc)$ {

allow 127.0.0.1;

deny all;

}

location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {

expires max;

log_not_found off;

}

location /lists/config {

deny all;

}

location ~* (index\.php|upload\.php|connector\.php|dl\.php|ut\.php|lt\.php|download\.php)$ {

root /var/www/lists.example.com;

fastcgi_split_path_info ^(.|\.php)(/.+)$;

include /etc/nginx/fastcgi_params;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;

}

# Block access to all other php files

location ~ \.php$ {

deny all;

}

location = /lists/admin {

auth_basic ‘Restricted’;

auth_basic_user_file /etc/nginx/htpass/lists.example.com;

}

}

access_log /var/log/sites/lists.example.com/access.log;

error_log /var/log/nginx/lists.example.com.error.log notice;

}

Replace example.com with the main domain hosted on your server. Create the access log directory:

mkdir /var/log/sites/lists.example.com

We have set up HTTP authentication for the lists/admin directory, to increase the security of phpList. Create the /etc/nginx/htpass/lists.example.com password file. If you want to give access to the administration page of phpList to the user gerald, run: htpasswd -c /etc/nginx/htpass/lists.example.com gerald

To add the user verner to the same password file run: htpasswd /etc/nginx/htpass/lists.example.com verner Change ownership and permissions for the password file: chown www-data:root /etc/nginx/htpass/lists.example.com chmod 400 /etc/nginx/htpass/lists.example.com

Restart Nginx:

systemctl restart nginx

23.5. Configure logrotate to rotate the phpList access logs

nano /etc/logrotate.d/nginx

Add the following section at the bottom of this file:

/var/log/sites/lists.example.com/access.log {

missingok

rotate 10

compress

delaycompress

notifempty

create 0640 www-data adm

size 2M

sharedscripts

prerotate

if [ -d /etc/logrotate.d/httpd-prerotate ]; then \

run-parts /etc/logrotate.d/httpd-prerotate; \

fi; \

endscript

postrotate

[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`

endscript

}

Replace example.com with the main domain hosted on your server.

23.6. Configure phpList using the web interface

Now you can access the phpList administration interface at:

https://lists.example.com/lists/admin

The first screen will inform you that the database has not been initialised:

To initialise it click on the ‘Initialise Database’ link. The next screen will look like this:

This form specifies that ‘The initial login name will be “admin”‘. You can leave the name and organisation fields empty, then enter the email address and password that you want to use as administrator, then click ‘Continue’.

In the next screen, you will be informed that the tables of the database have been created and you will have the option to subscribe to the phpList newsletter with the email address provided earlier. You can click ‘Subscribe’, but you can also click ‘Do not subscribe’:

After you click ‘Subscribe’ or ‘Do not subscribe’, you will be able to click on the ‘phpList Setup’ button:

After you click on ‘phpList Setup’, you will see the ‘configuration steps’ screen:

To start your settings verification click on ‘Verify Settings’. You will see the following screen, where you should enter admin as user and the password set up earlier:

In the next screen, you can edit any fields by clicking on the edit icon located above the fields that you want to modify.

Under ‘general settings‘ edit ‘Name of the organisation‘, because the content of this field will be displayed in the header of the subscribe page, which will be publicly accessible.

Under ‘campaign settings‘ edit the ‘Default for ‘From:’ in a campaign‘ field, entering the email address that you want your subscribers to see in the ‘From:’ field of the emails they receive from you. Also, edit the ‘Analytics tracking code to add to campaign URLs field and change ‘Google Analytics’ to ‘Matomo’.

It’s a good idea to change the ‘Default footer for sending a campaign. Replace the default text with a text similar to this:

<div class="footer" style="text-align:left; font-size: 85%;"> <p>You received this message because you are a subscriber of our mailing list.</p> <p>To update your details and preferences visit your personal <a href="[PREFERENCESURL]">preferences page.</a><br/><br/> You can unsubscribe from all our mailing lists by <a href="[UNSUBSCRIBEURL]">clicking here.</a></p> </div>

InWidth for Wordwrap of Text messages‘ you can change the width of the wordwrap for text messages from 75 to 85.

Under ‘transactional settings‘ you can edit the ‘From email address for system messagesand the ‘Reply-to email address for system messages.

It’s a good idea to change the ‘Message subscribers receive when they sign up. Replace the default text with a text similar to this:

Your email address has been added to our system and if you confirm, it will be included in the following mailing list:

[LISTS]

To offer our services we will need to:

– Transfer your email address to lists.example.com.

– Store your email address in your lists.example.com account.

– Send you emails from mail.example.com.

– Track your interactions with these emails for marketing purposes.

You can click the link from below to confirm that the email address used to subscribe is yours and that you want to have it included in our mailing list. By clicking on the link from below you also akcnowledge that you agree to our ‘Terms and Conditions'(https://www.domain.com/terms-and-conditions/) and our ‘Privacy Policy’ (https://www.domain.com/privacy-policy/) The confirmation link is:

[CONFIRMATIONURL]

If you don’t want your email address to be included in our mailing list or you do not agree to the above stated requirements, you should take no action and you can delete this message. If you don’t confirm your email address, it will be removed from our system in 30 to 60 days.

Replace example.com with the main domain hosted on your server, https://www.domain.com/terms-and-conditions/ with the URL of the ‘Terms and Conditions’ page and https://www.domain.com/privacy-policy/ with the URL of the ‘Privacy Policy’ page. It’s important to inform your subscribers about the way their email address will be handled, and to ask them to agree to your ‘Terms and Conditions’ and ‘Privacy Policy’. This will also ensure that your opt-in process complies with the GDPR (General Data Protection Regulation).

It’s also a good idea to change theMessage subscribers receive when they unsubscribe. Replace the default text with a text similar to this:

You have been unsubscribed from our mailing lists.

This is the last email you will receive from us. Our newsletter system will refuse to send you any further messages.

Your email address will be permanently deleted from our system in 30 to 60 days.

You can always re-subscribe by going to [SUBSCRIBEURL] and following the steps.

Thank you.

Then you can change the ‘Subject of the message subscribers receive after confirming their email address‘ by replacing the default text with:

Welcome to our mailing list!

You can also change the ‘Message subscribers receive after confirming their email address‘. Replace the default text with a text similar to this:

Welcome to our Newsletter!

Please keep this message for later reference.

Your email address has been added to the following mailing list(s):

[LISTS]

To update your details and preferences please go to [PREFERENCESURL].

If you do not want to receive any more messages, you can unsubscribe by clicking on [UNSUBSCRIBEURL].

Thank you.

You can change the ‘Message content subscribers receive when they have changed their details‘. Replace the default text with a text similar to this:

This message is to inform you of a change of your details in our mailing lists database. You are currently member of the following mailing lists: [LISTS] [CONFIRMATIONINFO] The information on our system about you is as follows: [USERDATA] If this is not correct, please update your information by going to: [PREFERENCESURL] Thank you.

You can replace ‘Part of the message that is sent to their old email address when subscribers change their information, and the email address has changed‘. Replace the default text with a text similar to this:

Please Note: when updating your details, your email address has changed. A message has been sent to your new email address with a URL to confirm this change. Please click on that URL to activate your membership.

Replace the default text of ‘Subject of message to send when subscribers request their personal location‘ with a text similar to this:

Your personal preferences page

Replace the default text of ‘Message to send when they request their personal location‘ with a text similar to this:

You have asked us to provide your personal preferences page in order to update your details.

The URL of your personal preferences page is mentioned below. Please make sure that you use the full URL.

Your personal preferences page is:

[PREFERENCESURL]

Thank you.

Under 'subscription-ui settings', under 'If there is only one visible list, should it be hidden in the page and automatically subscribe users who sign up' change 'Yes' to 'No'.

23.7. Create a subscribe page

On the left panel click on ‘Config’ > ‘Subscribe pages‘. You will see this screen:

Click on the ‘Add a new subscribe page’ button in the upper right corner.

On the new screen, under ‘General Information’ you can modify the default content and display of the subscription page, or you can leave everything as it is. You can also modify the transaction messages: the message that the subscriber receives when they subscribe, when they unsubscribe etc. It’s recommended to change the default text displayed when the subscription with an AJAX request has been successfull: under Text to display when subscription with an AJAX request was successful replace the default text with a message similar to this:

<h3>Thank you, your email address has been added to our system</h3><p>You will receive an email to confirm your subscription. Please click the link in the email to confirm.</p>

Then, under ‘Display email address confirmation field’ choose ‘Don’t display email address confirmation field’. Under ‘Select the lists to offer’, under ‘Lists’ select the mailing list(s) that you want to display on the subscribe page, by checking their checkbox.

To save the new subscribe page click on the ‘Save Changes’ button.

The URL with the link to the subscribe page, unsubscribe page and ‘update your preferences’ page, will be:

https://lists.example.com/lists

23.8. Delete the unconfirmed subscribers

It’s recommended to delete the unconfirmed subscribers from time to time. The unconfirmed subscribers are the subscribers who didn’t confirm their email address by clicking on the link inside the confirmation request email. If you use the exact text presented above for your transaction messages, you promise to your subscribers who don’t confirm their email address, to delete their email address in 30 to 60 days.

23.8.1. Manually delete the unconfirmed subscribers

You can manually delete all the subscribers who have not confirmed their subscription and have signed up between two specific dates, by going to ‘Subscribers’ > ‘Reconcile subscribers’, scrolling down to the ‘Delete subscribers who signed up and have not confirmed their subscription‘ section, selecting the date the subscribers signed up after and the date they signed up before, then clicking on the ‘Click here’ button. However, it’s much better to automate the process of deleting the unconfirmed subscribers, as shown below.

23.8.2. Automatically delete the unconfirmed subscribers

It’s a good idea to automatically delete the unconfirmed subscribers once a month. Since you may want to see the email addresses of the most recent unconfirmed subscribers, you can create a script and a cron job to delete only the unconfirmed subscribers who signed up between 60 and 30 days before the current date. In this way you will always have the email addresses of the unconfirmed subscribers who signed up in the previous month.

First, you have to add to phpList the ability to delete the unconfirmed subscribers in command line. To achieve this, navigate to /var/www/lists.example.com/lists/admin/actions:

cd /var/www/lists.example.com/lists/admin/actions

Create a new file called deleteunconfirmedsubscribers.php:

nano deleteunconfirmedsubscribers.php

Add the following content inside this file:

<?php

require_once dirname(__FILE__).’/../accesscheck.php’;

$fromval_init = $_GET[‘fromval’];

$toval_init = $_GET[‘toval’];

if (!isset($_GET[‘fromval’]) || $_GET[‘fromval’] == ”) {

$status = ‘The date the subscribers signed up after is required. Eg.: fromval=2020-09-12’;

return;

}

if (!isset($_GET[‘toval’]) || $_GET[‘toval’] == ”) {

$status = ‘The date the subscribers signed up before is required. Eg.: toval=2020-10-08’;

return;

}

$fromval = $fromval_init . ‘ 00:00:00’;

$toval = $toval_init . ‘ 23:59:59’;

// Delete all the unconfirmed subscribers that subscribed between the ‘fromval’ and ‘toval’ dates

$status = s(‘ Deleting unconfirmed subscribers: ‘).'<br/ >’;

flush();

$req = Sql_Query(sprintf(‘select id from %s where entered >= “%s” and entered <= “%s” and !confirmed’, $tables[‘user’], $fromval, $toval ));

$c = 0;

while ($row = Sql_Fetch_Array($req)) {

set_time_limit(60);

++$c;

deleteUserIncludeBlacklist($row[‘id’]);

}

$status .= $c.’ ‘.s(‘subscribers deleted successfully.’).”<br/>\n”;

Change permissions for this file to match the general permissions for /var/www:

chmod 660 deleteunconfirmedsubscribers.php

From now on, you can delete the unconfirmed subscribers who signed up between two specified dates by running commands like the following:

php /var/www/lists.example.com/lists/admin/index.php fromval=2020-07-01 toval=2020-10-25 command=deleteunconfirmedsubscribers -c /var/www/lists.example.com/lists/config/config.php -pruncommand

Replace example.com with the main domain hosted on your server, 2020-07-01 with the date after which the subscribers signed up and 2020-10-25 with the date before which the subscribers signed up.

Next, create the script that the cron job will run:

nano /srv/scripts/phplistdelunconfirmed

Add the following content inside this file:

#!/bin/bash

sixtydaysbefore=$(date +%Y-%m-%d -d “60 day ago”)

thirtydaysbefore=$(date +%Y-%m-%d -d “30 day ago”)

php /var/www/lists.example.com/lists/admin/index.php fromval=$sixtydaysbefore toval=$thirtydaysbefore command=deleteunconfirmedsubscribers -c /var/www/lists.example.com/lists/config/config.php -pruncommand

Replace example.com with the main domain hosted on your server. Change permissions for this script:

chmod 700 /srv/scripts/phplistdelunconfirmed

Then create the cron job:

crontab -e

Add the following lines at the bottom of the file:

# Delete all the phpList unconfirmed subscribers who subscribed up to 30 days before current time, at 2:25 AM, every 30 days

25 2 */30 * * /srv/scripts/phplistdelunconfirmed > /dev/null 2>&1

The cron job from above will run the /srv/scripts/phplistdelunconfirmed script every 30 days, at 2:25 AM. In this way, every 30 days, all the unconfirmed subscribers who subscribed up to 30 days before current time, will be automatically deleted.

23.9. Delete the blacklisted subscribers

When a subscriber unsubscribes, (s)he will be automatically blacklisted and no other emails will be sent to her/him in the future. A subscriber can also be blacklisted manually by the list administrator, from the subscriber’s profile (‘Subscribers’ > ‘Search subscribers’ > click on the email address of a subscriber > on the ‘Details’ tab click on the ‘Add to blacklist’ button) or automatically by phpList, when emails sent to them bounced too many times consecutively.

When you go to ‘Subscribers’ > ‘Subscriber lists’ and click on the name of a mailing list, you will see all the subscribers to that list. On the ‘Confirmed’ tab there will be the list of active, confirmed subscribers, and on the ‘Unconfirmed’ tab you will see all the subscribers who haven’t confirmed their email address after signing up, but also all the blacklisted subscribers. Thus, even they can be confirmed subscribers, the blacklisted subscribers will be listed on the ‘Unconfirmed’ tab. The idea is that you will have to manually or automatically remove the blacklisted subscribers periodically.

23.9.1. Manually delete the blacklisted subscribers

To delete all the blacklisted subscribers manually go to ‘Subscribers’ > ‘Reconcile subscribers’ > click on the ‘Delete all blacklisted subscribers’ button. If you want to delete only the subscribers who are blacklisted because they unsubscribed click on the ‘Delete subscribers who are blacklisted because they unsubscribed’ button.

23.9.2. Automatically delete the blacklisted subscribers

To automatically delete all the blacklisted subscribers (those manually blacklisted and those blacklisted because they unsubscribed or because the emails sent to them bounced too many times consecutively), first you have to add to phpList the ability to delete blacklisted subscribers in command line. To achieve this, navigate to /var/www/lists.example.com/lists/admin/actions:

cd /var/www/lists.example.com/lists/admin/actions

Create a new file called deleteblacklistedsubscribers.php:

nano deleteblacklistedsubscribers.php

Add the following content inside this file:

<?php

require_once dirname(__FILE__).’/../accesscheck.php’;

// Delete subscribers who are blacklisted (for any reason)

$status = s(‘Deleting blacklisted subscribers: ‘).'<br/ >’;

flush();

$req = Sql_Query(‘

SELECT

id

FROM

‘.$tables[‘user’].’

WHERE

blacklisted = 1′

);

$c = 0;

while ($row = Sql_Fetch_Array($req)) {

set_time_limit(60);

++$c;

deleteUserIncludeBlacklist($row[‘id’]);

}

$status .= $c.’ ‘.s(‘subscribers deleted successfully.’).”<br/>\n”;

From now on, you can delete all the blacklisted subscribers by running commands like the following:

php /var/www/lists.example.com/lists/admin/index.php command=deleteblacklistedsubscribers -c /var/www/lists.example.com/lists/config/config.php -pruncommand

Replace example.com with the main domain hosted on your server.

Next, create the script that the cron job will run:

nano /srv/scripts/phplistdelblacklisted

Add the following content inside this file:

#!/bin/bash

php /var/www/lists.example.com/lists/admin/index.php command=deleteblacklistedsubscribers -c /var/www/lists.example.com/lists/config/config.php -pruncommand

Replace example.com with the main domain hosted on your server. Change permissions for this script:

chmod 700 /srv/scripts/phplistdelblacklisted

Then create the cron job:

crontab -e

Add the following lines at the bottom of the file:

# Delete all the phpList blacklisted subscribers at 3:25 AM, on the second day of January, April, July and October

25 3 2 1,4,7,10 * /srv/scripts/phplistdelunconfirmed > /dev/null 2>&1

The cron job from above will run the /srv/scripts/phplistdelblacklisted script at 3:25 AM, on the second day of January, April, July and October. In this way, every 3 months, all the blacklisted subscribers will be automatically deleted.

23.10. Add a new administrator and disable the ‘admin’ account

As everyone should know, ‘admin’ is the worst username that one can possibly choose, to log in to any application. As mentioned before, by default phpList sets ‘admin’ as the username of the phpList administrator. To change this default username, first log in, then, on the left panel click on ‘Config’ > ‘Manage administrators’ > ‘Add new admin’. You will see the following screen:

In the ‘Login name’ field enter the new username, in the ’email’ field enter the email address, in the ‘Choose how to set password’ field choose ‘Create password’; this will make the ‘Create password’ and ‘confirm password’ fields to appear below, so enter your password in these fields; in the ‘Is this admin Super Admin?‘ choose ‘Yes’; in the ‘Privileges’ field check all the checkboxes to give the new administrator all the privileges, then click ‘Save changes’.

Next, on the left panel click ‘Logout’ to log out, then log in with your new username and password, then go again to ‘Config’ > ‘Manage administrators’. You will see a list with two administrators: ‘admin’ and the administrator that you have just created. You should delete the ‘admin’ account by clicking on the ‘Del’ link on the ‘admin’ line.

23.11. Create a mailing list

To create a new mailing list, on the left panel click on ‘Subscribers’ > ‘Subscriber lists’ > ‘Add a list’:

In the new screen, in the ‘List name’ field add a name for the new list, check the ‘Public’ checkbox if you want the list to be public and displayed on the subscribe page, in the ‘Order for listing’ field you can enter a natural number (0 or greater) to indicate the order of display in the list of available lists, in the ‘List Description’ field enter a short description, then click on ‘Save’.

23.12. Manually add subscribers to a list

In order to test your installation you will need to add some subscribers to a list. You can manually add some of your own email addresses as subscribers, to a list that you created earlier. On the left panel click on ‘Config’ > ‘Configuration’, to return to the ‘Configuration steps’ screen. There click on ‘Add some subscribers’ to manually add subscribers to a list. You will see the following screen:

Here you can choose the method to add subscribers: ‘copy and paste list of emails’, ‘import by uploading a file with emails’, ‘import by uploading a CSV file with emails and additional data’. If you choose the ‘copy and paste list of emails’ method, you will see the following screen:

Here you can choose the list to add subscribers to, then you can enter the emails of the subscribers, one per line, in the text area under Please enter the emails to import …‘, then click on the ‘Import emails’ button.

Please note that the typical way of adding subscribers to a list is to direct them to the subscribe page (https://lists.example.com/lists) where they can subscribe by entering their email address.

23.13. Add the default campaign templates

Please note that if you click on Campaigns > Manage campaign templates, you will see a button with the text ‘Add templates from default selection’, in the upper left corner of the screen. If you click on it, you will be able to add the default templates suite:

Select ‘System template’, and click on the ‘Select’ button, then, to also add the other template, select ‘Simple responsive template’ and click the ‘Select’ button again. Then, if you click on Campaigns > Manage campaign templates, you will see a list containing the two templates that you just added:

You can select ‘Simple responsive template’ as ‘Campaign Default’ and ‘System template’ as ‘System’.

When a user subscribes to a mailing list, phpList will automatically send them an email, asking them to confirm. phpList refers to this email message as a ‘system’ message. Other ‘system’ messages are: the welcome email, the updated preferences email and the unsubscribe email. If you select the templates as shown above, all the ‘system’ messages will use the ‘System template’ template, while the default template for email campaigns will be the ‘Simple responsive template’.

23.14. Create a campaign template

To create a campaign template, on the left panel click on ‘Campaigns > Manage campaign templates > Add new template. You will see the following screen:

Here you can click on Source in the upper left corner of the text area and write the HTML template from scratch, or you can import a template from your computer by clicking Browse. You can download free phpList campaign templates from sources like this: https://github.com/phpList/phplist-templates . Then you need to give a title to the template and click Save Changes.

If you use the ‘Simple responsive template’ to send a test newsletter, you will see that it has a lot of problems: the wrapper for the message body is too narrow, the font for the body and footer is too small, the default lines added to the footer are not needed, etc. If you want to have a default campaign template that looks professional, you can use the method described above to create your own template using the following code:

<!doctype html>

<html>

<head><meta name=”viewport” content=”width=device-width” /><meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8″ />

<title>[SUBJECT]</title>

<style type=”text/css”>

img {

border: none;

-ms-interpolation-mode: bicubic;

max-width: 100%;

}

/* ————————————-

BODY & CONTAINER

————————————- */

.body {

background-color: #f6f6f6;

width: 100%;

}

/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */

.templatecontainer {

display: block;

margin: 0px auto !important;

/* makes it centered */

max-width: 680px;

padding: 0px 10px 10px 10px;

width: 680px;

}

.logo {

padding-bottom: 10px;

padding-top: 0px;

}

/* This should also be a block element, so that it will fill 100% of the .container */

.templatecontent {

box-sizing: border-box;

display: block;

margin: 0 auto;

max-width: 680px;

padding: 0px 10px 10px 10px;

}

/* ————————————-

HEADER, FOOTER, MAIN

————————————- */

.main {

background: #ffffff;

border-radius: 3px;

width: 100%;

}

.wrapper {

box-sizing: border-box;

padding: 20px;

}

.templatecontent-block {

padding-bottom: 10px;

padding-top: 0px;

margin-top: 0px;

}

.footer {

clear: both;

margin-top: 10px;

text-align: center;

width: 100%;

}

.footer td,

.footer p,

.footer span,

.footer a {

color: #5c5c5c;

font-size: 14px;

text-align: center;

}

/* ————————————-

TYPOGRAPHY

————————————- */

h1,

h2,

h3,

h4 {

color: #000000;

font-family: sans-serif;

font-weight: 400;

line-height: 1.4;

margin: 0;

margin-bottom: 30px;

}

h1 {

font-size: 35px;

font-weight: 300;

text-align: center;

text-transform: capitalize;

}

p,

ul,

ol {

font-family: sans-serif;

font-size: 16px;

font-weight: normal;

margin: 0px;

margin-bottom: 15px;

}

p li,

ul li,

ol li {

list-style-position: inside;

margin-left: 5px;

}

/* ————————————-

BUTTONS

————————————- */

.templatebtn {

box-sizing: border-box;

width: 100%; }

.templatebtn > tbody > tr > td {

padding-bottom: 15px; }

.templatebtn table {

width: auto;

}

.templatebtn table td {

background-color: #ffffff;

border-radius: 5px;

text-align: center;

}

.templatebtn a {

background-color: #ffffff;

border: solid 1px #3498db;

border-radius: 5px;

box-sizing: border-box;

color: #3498db;

cursor: pointer;

display: inline-block;

font-size: 16px;

font-weight: bold;

margin: 0;

padding: 12px 25px;

text-decoration: none;

text-transform: capitalize;

}

.templatebtn-primary table td {

background-color: #3498db;

}

.templatebtn-primary a {

background-color: #3498db;

border-color: #3498db;

color: #ffffff;

}

/* ————————————-

OTHER STYLES THAT MIGHT BE USEFUL

————————————- */

.last {

margin-bottom: 0;

}

.first {

margin-top: 0;

}

.align-center {

text-align: center;

}

.align-right {

text-align: right;

}

.align-left {

text-align: left;

}

.clear {

clear: both;

}

.mt0 {

margin-top: 0;

}

.mb0 {

margin-bottom: 0;

}

.preheader {

color: transparent;

display: none;

height: 0;

max-height: 0;

max-width: 0;

opacity: 0;

overflow: hidden;

mso-hide: all;

visibility: hidden;

width: 0;

}

.powered-by a {

text-decoration: none;

}

hr {

border: 0;

border-bottom: 1px solid #f6f6f6;

margin: 20px 0;

}

/* ————————————-

RESPONSIVE AND MOBILE FRIENDLY STYLES

————————————- */

@media only screen and (max-width: 620px) {

table[class=body] h1 {

font-size: 28px !important;

margin-bottom: 10px !important;

}

table[class=body] p,

table[class=body] ul,

table[class=body] ol,

table[class=body] td,

table[class=body] span,

table[class=body] a {

font-size: 16px !important;

}

table[class=body] .wrapper,

table[class=body] .article {

padding: 10px !important;

}

table[class=body] .templatecontent {

padding: 0 !important;

}

table[class=body] .templatecontainer {

padding: 0 !important;

width: 100% !important;

}

table[class=body] .main {

border-left-width: 0 !important;

border-radius: 0 !important;

border-right-width: 0 !important;

}

table[class=body] .templatebtn table {

width: 100% !important;

}

table[class=body] .templatebtn a {

width: 100% !important;

}

table[class=body] .img-responsive {

height: auto !important;

max-width: 100% !important;

width: auto !important;

}

}

/* ————————————-

PRESERVE THESE STYLES IN THE HEAD

————————————- */

@media all {

.ExternalClass {

width: 100%;

}

.ExternalClass,

.ExternalClass p,

.ExternalClass span,

.ExternalClass font,

.ExternalClass td,

.ExternalClass div {

line-height: 100%;

}

.apple-link a {

font-family: inherit !important;

font-size: inherit !important;

font-weight: bold !important;

line-height: inherit !important;

}

.templatebtn-primary table td:hover {

background-color: #34495e !important;

}

.templatebtn-primary a:hover {

background-color: #34495e !important;

border-color: #34495e !important;

}

}

</style>

</head>

<body style=” background-color: #f6f6f6;font-family: sans-serif;-webkit-font-smoothing: antialiased;font-size: 16px;line-height: 1.4;margin: 0;padding: 0;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;”>

<table border=”0″ cellpadding=”0″ cellspacing=”0″ class=”body” role=”presentation” style=”border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;”>

<tbody>

<tr>

<td style=”font-family: sans-serif;font-size: 16px;vertical-align: top;”>&nbsp;</td>

<td class=”container” style=”font-family: sans-serif;font-size: 16px;vertical-align: top;”>

<div class=”templatecontent”>

<div class=”logo”>

<center><img src=”[LOGO]” /></center>

</div>

<!– START CENTERED WHITE CONTAINER –>

<table class=”main” role=”presentation” style=”border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;”><!– START MAIN CONTENT AREA –>

<tbody>

<tr>

<td class=”wrapper” style=”font-family: sans-serif;font-size: 16px;vertical-align: top;”>

<table border=”0″ cellpadding=”0″ cellspacing=”0″ role=”presentation” style=”border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;”>

<tbody>

<tr>

<td style=”font-family: sans-serif;font-size: 16px;vertical-align: top;”><span class=”preheader”>This is preheader text. Some clients will show this text as a preview.</span> [CONTENT]</td>

</tr>

</tbody>

</table>

</td>

</tr>

<!– END MAIN CONTENT AREA –>

</tbody>

</table>

<!– START FOOTER –>

<div class=”footer”>

<table border=”0″ cellpadding=”0″ cellspacing=”0″ role=”presentation” style=”border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;”>

<tbody>

<tr>

<td class=”templatecontent-block” style=”font-family: sans-serif;font-size: 15px;vertical-align: top;”><span class=”apple-link”>[FOOTER]</span></td>

</tr>

</tbody>

</table>

</div>

<!– END FOOTER –><!– END CENTERED WHITE CONTAINER –></div>

</td>

<td>&nbsp;</td>

</tr>

</tbody>

</table>

</body>

</html>

You can call this template ‘Simple Newsletter Template’ and you can make it the default campaign template.

23.15. Manage bounces

Before sending any mass emails, you have to consider the fact that some of the email addresses provided by subscribers may become invalid for various reasons: the domain is suspended, the mail server bacomes unavailable because of hardware or software malfunction, etc. The emails sent by phpList to such email addresses will bounce and you will receive an ‘Undelivered Mail Returned to Sender’ message in the mailbox designated to receive bounces (bounces@example.com) . The most responsible way to deal with bounced emails is to create bounce rules, so that phpList will know what to do if the emails sent to certain email addresses could not be delivered. The thing to keep in mind here is that if you repeatedly try to deliver emails to addresses that bounce emails, the IP of your server can be blacklisted.

To create bounce rules go to ‘System’ > ‘Manage bounces’ > click on the ‘List Bounce Rules’ button. On the ‘Active’ tab you will see the list with the active bounce rules, which will be empty if you have just installed phpList. As a starting point, you should add the following rules (recommended in the official phpList documentation here: https://www.phplist.org/manual/books/phplist-manual/page/bounce-management):

Under ‘add a new rule’, in the ‘Regular Expression field enter:

No such person at this address

From the ‘Action’ drop-down list choose:

Delete subscriber

Click on ‘Add new Rule’.

In a similar manner, add the following ‘Regular Expression’ ‘Action’ pairs:

Regular Expression:

(Archived recipient.)

Action:

Delete subscriber

Regular Expression:

(delivery error: dd This user doesn’t have a yahoo.fr account|This user doesn’t have a yahoo\.fr account)

Action:

Delete subscriber

Regular Expression:

(type=MX: Host not found)

Action:

Delete subscriber

Regular Expression:

(sorry, no mailbox here by that name|Mailbox disabled|address not found in table|User mailbox is not local|mailbox not allowed|Mailbox syntax incorrect|RESOLVER\.ADR\.RecipNotFound)

Action:

Delete subscriber and bounce

Regular Expression:

(account is disabled|This mailbox has been blocked due to inactivity|This account has been disabled|The email account that you tried to reach does not exist|This is a permanent error .* local delivery failed)

Action:

Delete subscriber and bounce

Regular Expression:

(User unknown|Unknown user|Unknown address|No such recipient|No such user|The email account that you tried to reach is disabled|Recipient not found|Recipient unknown|Invalid recipient|Address unknown|Recipient address rejected)

Action:

Delete subscriber and bounce

When you start sending mass emails and you receive various ‘Undelivered Mail Returned to Sender’ messages in the bounces@example.com mailbox, you can read these emails to find new types of messages sent by the servers that bounce your emails. This messages are usually listed after:

host domain.com[111.111.111.111] said: …

where 111.111.111.111 is the IP of the recipient’s SMTP server. Based on these messages, you can create new bounce rules and add them as described above.

Please note that after you receive bounces, they won’t be displayed in ‘System’ > ‘Manage bounces’ > ‘View Bounces’ or in ‘Campaigns’ > ‘List of campaigns’ > ‘Bounced’, until you ‘process’ them. We’ll show below how to process bounces automatically, by setting up a cron job.

23.16. Limit retries of failed messages

Many SMTP servers are configured in such a way that they don’t bounce incoming emails for email addresses that don’t exist on the server. They just reject such emails. This is done for security reasons, since spammers usually forge the ‘From’ address of the emails that they send, and if the SMTP server that receives such spam emails bounced the emails, the ‘Undelivered Mail Returned to Sender’ message would go to the forged ‘From’ address creating artificial SMTP traffic. In this way, the legitimate receiving SMTP server could become a spam suspect.

The problem is that when phpList tries to send a message to a confirmed email address whose host SMTP server responds with a rejection and not with a bounce, phpList keeps trying to send the email over and over again, and never finishes sending the current campaign. It enters in a never-ending loop. To solve this problem we can tell phpList to ‘unconfirm’ the email addresses that reject messages, so that no other emails will be sent to them. To achieve this edit the /var/www/lists.example.com/lists/admin/actions/processqueue.php file:

nano /var/www/lists.example.com/lists/admin/actions/processqueue.php

Search for the following block:

if (!$throttled && !validateEmail($useremail)) {

++$unconfirmed;

++$counters[’email address invalidated’];

logEvent(“invalid email address $useremail user marked unconfirmed”);

Sql_Query(sprintf(‘update %s set confirmed = 0 where email = “%s”‘,

$GLOBALS[‘tables’][‘user’], $useremail));

}

}

Comment out the two lines in bold, as shown below:

// if (!$throttled && !validateEmail($useremail)) {

++$unconfirmed;

++$counters[’email address invalidated’];

logEvent(“invalid email address $useremail user marked unconfirmed”);

Sql_Query(sprintf(‘update %s set confirmed = 0 where email = “%s”‘,

$GLOBALS[‘tables’][‘user’], $useremail));

}

// }

Please note that after you upgrade phpList, you will have to make this change again.

23.17. Limit email sending rate

Although in general, for VPSs, cloud servers and dedicated servers, the hosting company doesn’t impose any email sending limitations, it’s possible that they have a restriction for the maximum number of emails that can be sent from one IP, per day or per hour.

A second type of email sending limitation is that imposed by the company that hosts the receiving SMTP servers. For example, a company like Microsoft may impose a limit on the maximum number of emails that a single IP can send to different hotmail.com accounts in one hour. Thus, if you have a mailing list with a lot of hotmail.com email accounts and you try to send a newsletter as fast as possible to all the subscribers of that list, your IP can get blacklisted by Microsoft for spam sending behavior.

To avoid the two types of limitations mentioned above, it’s recommended to set up the phpList campaigns, so that the emails are sent in small batches over a wider period of time, or one by one, with a certain delay between consecutive emails.

For example, let’s say your hosting provider doesn’t let you send more than 400 emails per hour and you have a mailing list with 1500 subscribers. If you configure phpList as we explained above and you don’t change the default values of the MAILQUEUE_BATCH_SIZE, MAILQUEUE_BATCH_PERIOD, MAILQUEUE_THROTTLE parameters, phpList will try to send all the 1500 emails in the first hour and because of the limitation imposed by the hosting provider, only the first 400 will be sent, while the other 1100 will fail. In this situation, you should edit the /srv/scripts/phplist.php file:

nano /srv/scripts/phplist.php

Add the following lines at the bottom of the file:

define("MAILQUEUE_BATCH_SIZE",0); define("MAILQUEUE_BATCH_PERIOD",3600); define('MAILQUEUE_THROTTLE',10); 

The parameters from above prevent phpList from sending all the emails of a campaign in one go. Instead, it will send them in several batches.

MAILQUEUE_BATCH_SIZE – definess the number of emails that are allowed to be sent in one batch; 0 means that there is no limit to the number of emails allowed to be sent in one batch.

MAILQUEUE_BATCH_PERIOD – defines the time interval (in seconds) in which one batch of emails can be sent. In the case presented above, each batch can be sent in one hour (3600 seconds).

MAILQUEUE_THROTTLE - defines the delay (in seconds) between two consecutive emails. Fractions of a second can be used. 0.5 will mean half of a second. Thus, the settings from above instruct phpList to send the emails in batches of one hour, all the emails being separated by an interval of 10 seconds. This means that in every 3600 seconds, phpList will send emails 10 seconds apart, which will give a total of 360 emails per hour. In this way, the 1500 emails will be sent in 4.17 hours and the limit of 400 emails per hour, imposed by the hosting provider, will be respected. Please note that if the limit is 400 emails per hour, you shouldn't configure phpList to send exactly 400 per hour. You should leave a margin of about 40 emails per hour, because while phpList is sending a campaign, you may need to send other additional emails, such as subscription confirmation requests, regular personal or business emails, etc. So, for a limit of 400 emails per hour, its safe to configure phpList to send about 360 emails per hour. The same thing can be achieved by using the following settings: define("MAILQUEUE_BATCH_SIZE",30); define("MAILQUEUE_BATCH_PERIOD",300); These settings instruct phpList to send the emails in batches of 30 emails, every 5 minutes (300 seconds). This means that the sending rate will be limited to 360 emails per hour. In this way, the 1500 emails will be sent in 4.17 hours and the limit of 400 emails per hour will be respected. You can configure phpList in a similar way to send a specific number of emails per day. Thus, the MAILQUEUE_BATCH_PERIOD can be 86400 seconds  (24 hours). phpList can send about 5000 emails per hour, but this will obviously depend on the size of the emails, on the hardware specifications and current load of your server, on the hardware specifications and current load of the receiving SMTP servers, etc. The idea is that, as explained above, you shouldn't configure phpList to send as many emails as it can in one go. Instead, you should restrict the number of emails sent by phpList in a time unit, even if your hosting provider doesn't impose any restrictions on the email sending rate. Doing this, you will avoid having your IP blacklisted for spam-sending behavior, by the companies hosting the email accounts of your subscribers (Microsoft, Yahoo, AOL, etc.). 

23.18. Create a campaign

To send a mass email, such as a newsletter, you have to create a ‘campaign’. To create a new campaign go to Campaigns > Send a campaign, click on the ‘Start a new campaign’ button. You will see the following screen:

On the ‘Content’ tab, in the ‘Campaign subject‘ field enter the name of the new campaign, in the ‘From line’ and ‘Reply to’ fields enter the email address that you want your subscribers to see as the origin of the email message, in the ‘Compose message’ text area enter the text of the email, modify the footer if you need to, etc. You can scroll down to the ‘Send test’ section, enter an email address where you want to receive the test newsletter email, then click on ‘Send test’.

On the ‘Text’ tab, you can create a separate plain text version of the message. If you send the message in HTML format (by selecting ‘HTML’ on the ‘Format’ tab), phpList will send the message in both HTML format and plain text format. This is because some subscribers may open the message with email clients that are configured to display only plain text messages. Thus, if the subscriber’s email client can display HTML, it will display the HTML version of the message, if it can only display plain text, it will display the plain text version. For this second situation, it makes sense to create a separate, more elaborate plain text version of the message on the ‘Text’ tab, so as to ensure that certain elements of the text are displayed as closely as possible to the HTML bold words, numbered lists, bulleted lists, headers, etc. For example, a HTML bold word can be ‘reproduced’ in plain text as a word placed between asterisk, like this: *word*. If you think that all the subscribers will be able to see the HTML version of the message or you are satisfied with the default plain text version of the message, you can leave the ‘Text’ tab empty.

On the ‘Format’ tab you can choose to send the email in ‘HTML’ or ‘Text format’, you can choose a template from the available templates, you can set a ‘Campaign Title’ that will be displayed in the list of campaigns, and you can send a test email.

On the ‘Scheduling‘ tab, under Embargoed until‘, you have to enter a moment in time which is very important. The time set here is not the time when the newsletter will be sent but the time after which the newsletter can be sent if you add the campaign to the queue and then manually or automatically ‘process the queue’. So, the time set under Embargoed until is just the time until which it’s guaranteed that the newsletter won’t be sent. It means that the campaign will be allowed to start only after the specified moment in time, but it won’t be actually started at that moment.

On the ‘Lists’ tab you have to choose the list/lists of subscribers to which you will be sending the mass email. Check one or multiple checkboxes to select the list/lists.

On the ‘Finish’ tab you can configure an email address where you want to receive notifications when the campaign will start and when it will finish, you can add analytics tracking code in the code of the sent emails, etc.:

In order to send the campaign you have to first click on the ‘Place Campaign in Queue for Sending’ button. You will see the following screen:

Then, if the moment specified in ‘Embargoed until’ has been reached, you can process the queue manually (or automatically) to send the mass email.

To process the queue manually, just click on the ‘Process queue’ button. If you accidentally close the browser before the mailing campaign finishes, you can log in again to phpList, go to ‘System’ and click on ‘Send the queue’. This will process the queued campaign(s), sending the emails only to the subscribers to which it didn’t send the emails in the previous run. phpList has a mechanism that ensures that during a campaign, each subscriber receives the email only once, even if the campaign is interrupted and restarted later.

To process the queue and bounces automatically, you have to create a cron job. First of all make a script and place it in /srv/scripts, as described below:

cd /srv/scripts

nano phplistqueuebncs

Add the following lines inside the new file:

#!/bin/bash

php /var/www/lists.example.com/lists/admin/index.php -c /var/www/lists.example.com/lists/config/config.php $*

Replace example.com with the main domain hosted on your server. Change permissions for this script:

chmod 700 phplistqueuebncs

Now open crontab to add the cron jobs:

crontab -e

Add the following lines at the bottom:

# Process the phpList queue every Monday and Wednesday (1,3) at 4:15 AM

15 4 * * 1,3 /srv/scripts/phplistqueuebncs -pprocessqueue > /dev/null 2>&1

# Process phpList bounces at 2:45 AM on the first day of every month

45 2 1 * * /srv/scripts/phplistqueuebncs -pprocessbounces > /dev/null 2>&1

The settings from above will process the phpList queue every Monday and Wednesday at 4:15 AM and the bounces on the first day of every month. Please note that you can have multiple phpList campaigns in the queue. All the queued campaigns will be sent starting at 4:15 AM every Monday and Wednesday. Of course, you can change the days and hours when you send the queued campaigns. You can send mass emails three or four times a week, or you can send them just one time a week. You can also set up a cron job to process the phpList queue every 15 minutes or so, and then schedule each campaign on the phpList ‘Scheduling’ tab, knowing that they will be sent 15 minutes after the ‘Embargoed until’ moment. To process the queue every 15 minutes, the cron job will look like this:

# Process the phpList queue every 15 minutes

*/15 * * * * /srv/scripts/phplistqueuebncs -pprocessqueue > /dev/null 2>&1

You can process the queue even more frequently, like every 10 or 5 minutes, but whenever you set up a cron job, you have to consider that too frequent iterations of a process will burden the server and can waste system resources, especially if the respective process dosn’t really need to run so often. Thus, it’s recommended to set up cron jobs that run processes as rarely as possible. Since you only send a newsletter a few times per week, or one time per week, you can set up a cron job to process the phpList queue only on the specific day(s) and a specific hour.

The same reasoning applies to processing bounces. You can process bounces several times per month, but you can also process them once per month. Processing bounces will result in their listing in ‘System’ > ‘Manage bounces’ > ‘View Bounces’ and in the display of their number in ‘Campaigns’ > ‘List of campaigns’, in each list’s ‘Bounced’ section. Processing bounces will also empty the bounces@example.com mailbox. Thus, if you log in to bounces@example.com after the bounces have been processed, you will find an empty Inbox (and Trash), because all the messages will have been removed.

If after sending a campaign, you want to find out how many emails bounced, and the next automated bounce processing is scheduled to take place too far away in the future, you can process the bounces manually in command line by running:

/srv/scripts/phplistqueuebncs -pprocessbounces

To test if bounce processing works as expected, you can add bouncetest@tribulant.com to a test mailing list. Any email sent to that address will bounce and you will receive the ‘Underlivered Mail Returned to Sender’ message in your bounces@example.com mailbox.

23.19. Requeue a campaign

When you go to ‘Campaigns’ > ‘List of campaigns’, you will find that in the ‘Action’ column, each campaign has a ‘Requeue’ button. The requeue function is used to send a campaign to subscribers who join a mailing list after the campaign has been sent to that mailing list. A campaign will never be sent to the same subscriber twice. So, when you requeue a campaign after it is first sent, it will be sent again only to new subscribers.

23.20. Integrate phpList with WordPress

There are WordPress plugins that allow website owners to integrate phpList with their WordPress websites. Users can enter their email address and press a ‘Subscribe’ button on a web page, and after they confirm their email address by clicking on a link included in an email, their email address is automatically added as a confirmed address to a phpList mailing list. However, as in any other similar situation in which you want to add a functionality to a WordPress website, it’s preferable to avoid using a plugin when you can add the same functionality ‘manually’. This will ensure that you will have to maintain the smallest number of plugins possible and you will keep the website as light as possible.

Thus, the best method to integrate phpList with WordPress is to ‘manually’ add an AJAX form on your website, as we describe below.

To avoid getting the “Access to … has been blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header has a value … that is not equal to the supplied origin.” error when you try to subscribe on the WordPress website, you have to edit the phpList configuration file:

nano /srv/scripts/phplist.php

Add the following line at the bottom of the file:

define(‘ACCESS_CONTROL_ALLOW_ORIGIN’,’https://www.domain.com‘);

where https://www.domain.com is the website on which you want to display the AJAX form.

Next, log in to the WordPress website where you want to add the AJAX form (here we called it www.domain.com), go to ‘Appearance’ > ‘Customize’, then add a ‘Text’ widget to the ‘Right Sidebar’ or ‘Left Sidebar’ or ‘Footer Sidebar One’, or in other area that you prefer. How you will do this depends upon your theme but in general, the ‘Customize’ menu will have a ‘Widgets’ entry; if you click on it you can then click on the sidebar or the footer entry and once in that section you can click the ‘Add a Widget’ button, then select the ‘Text’ widget by clicking on it. In the ‘Title’ field of the ‘Text’ widget you can enter ‘Subscribe to our newsletter’ or a similar title, then click on the ‘Text’ tab of the text area and paste the code from below. Replace example.com with the main domain hosted on your server, but don’t add any spaces or new lines:

<form method=”post” name=”subscribeform” id=”subscribeform” enctype=”multipart/form-data”> <table> <tr> <td><div class=”newsletterEmail”>Email</div></td> <td class=”attributeinput”><input type=text name=”email” value=”” id=”nlEmail” size=”40″></td> </tr><tr id=”nlCountryRow”> <td><div class=”newsletterCountry”>Country</div></td> <td class=”attributeinputsec”><input type=text name=”country” value=”” id=”nlCountry” size=”40″></td> </tr> </table> <input type=hidden name=”htmlemail” value=”1″> <input type=”hidden” name=”” value=”signup” /> <input type=”hidden” name=”subscribe” value=”subscribe”/> <button class=’button’ onclick=”if (checkform()) {submitForm();} return false;” >Subscribe</button> <div id=”result” style=”color: red;”></div> </form>

<script type=”text/javascript”> function checkform() { if (jQuery(“#nlCountry”).val() != ”) { jQuery(‘#nlEmail’).val(”); } re = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/; if (!(re.test(jQuery(“#nlEmail”).val()))) { alert(‘Please enter a valid email address’); jQuery(“#nlEmail”).focus(); return false; } return true; } function submitForm() { successMessage = ‘Thank you for your registration. Please check your email to confirm.’; data = jQuery(‘#subscribeform’).serialize(); jQuery.ajax( { type: ‘POST’, data: data, url: ‘https://lists.example.com/lists/?p=asubscribe&id=1′, dataType: ‘html’, success: function (data, status, request) { jQuery(“#result”).empty().append(data != ” ? data : successMessage); jQuery(‘#nlEmail’).val(”); }, error: function (request, status, error) { alert(‘Sorry, we were unable to process your subscription.’); } }); } </script>

Please note that the id at the end of the subscription URL, https://lists.example.com/lists/?p=asubscribe&id=1 , is the id of the subscribe page (‘Config’ > ‘Subscribe pages’ > ID). Also the mentioned URL contains p=asubscribe, not p=subscribe like regular subscribe pages. If you look closely to the code from above, you will notice that it contains a ‘honeypot’ trap for spam bots. The text input field having the id nlCountry is not necessary for signing up. If somebody enters some text in that field, the content of the email text field, having the id nlEmail”, will be deleted and signing up will fail. The entire table row with the id “nlCountryRow” will be hidden by the use of CSS, as we’ll show below, so that the human visitors won’t see it and won’t enter any text into it. In this way legitimate visitors will leave the field empty and they will be able to sign up. However, regular spam bots that don’t have highly complex mechanisms to read and analyze the HTML, JavaScript and CSS code, will enter some text in the hidden nlCountry field and they won’t be able to sign up.

Click on ‘Done’, then click ‘Publish’. When the AJAX form is added to a sidebar, its default look will be similar to this:

To hide the ‘Country’ row and to make the ‘Email’ row look better, you should add the following CSS code to the style.css file of your active child theme (/var/www/domain.com/wp-content/themes/parenttheme-child/style.css):

#subscribeform table {

border: 0px;

margin: 0px;

padding: 0px;

}

#subscribeform td {

border: 0px;

margin: 0px;

padding: 0px;

}

#subscribeform .newsletterEmail {

color: #0fbe7c;

weight: 1000;

margin: 0px 4px 0px 0px;

}

#subscribeform #nlCountryRow {

display: none !important;

}

The CSS code from above will make the AJAX form look like this:

Of course, you can change the color of the ‘Email’ tag to make it match the color scheme of your website. Just change the ‘color’ parameter in:

#subscribeform .newsletterEmail {

color: #0fbe7c;

weight: 1000;

margin: 0px 4px 0px 0px;

}

You can further customize the look of this form by changing the font family, the font size, the color of the ‘Subscribe’ button, etc.

After the visitors enter their email address and click on ‘Subscribe’, they will see the following message:

Obviously, you can include the AJAX subscribe form on a regular web page instead of the sidebar or footer. All you have to do is to insert the code given above in a new ‘Custom HTML’ block on that web page. This means that after you click on the place where you want to add the form, you have to click on the ‘+’ sign in the upper left corner of the edit window, then scroll down to the ‘Custom HTML’ block and click on it. Next enter the code given above in the new block, replacing example.com with the main domain hosted on your server. Then click on the ‘Publish/Update’ button. If you want to insert the AJAX subscribe form both on a regular page and in the sidebar/footer, you’ll have to pay attention to the IDs of the form elements. Browsers won’t act correctly if two elements on a page have the same ID, so, you’ll have to change the IDs for the whole form, for the ‘Email’ field, for the ‘Country’ field, etc., so that the two forms which will be displayed simultaneously on a web page, won’t have identical IDs and the browsers will know how to display them correctly.

As we have suggested above, the correct way of adding email addresses to a mailing list is the ‘double opt-in’ method: all the subscribers have to enter their email address and press the ‘Subscribe’ button on a web page, then they have to confirm their email and their subscription by clicking a link included in an email sent automatically to their email address. The ‘double opt-in’ method is important because it’s a way to verify that the provided email belongs to the person who pressed the ‘Subscribe’ button and it proves that the person really wants to be included on your mailing list. Using this method, you will avoid having your emails categorized as spam by users and by different email providers.

GDPR also requires that you inform your subscribers about the way in which their data will be handled, on a ‘Privacy Policy’ page, when you ask them to subscribe to your mailing list. You can add a ‘Privacy Policy’ checkbox and a ‘Terms and Conditions’ checkbox to the AJAX subscription form, and request that the visitors agree to the ‘Privacy Policy’ and ‘Terms and Conditions’ before pressing the ‘Subscribe’ button, but since the subscribe form needs to be as simple as possible, it’s acceptable to include the links to your ‘Privacy Policy’ and ‘Terms and Conditions’ web pages in the confirmation request email, as we have shown above. In this way, the users can read the two web pages before confirming their email address and becoming active members of the mailing list.

23.21. Configure Fail2ban to protect phpList against brute-force attacks

Navigate to the /etc/fail2ban/filter.d directory:

cd /etc/fail2ban/filter.d

Create a file called phplist.conf:

nano phplist.conf

Add the following content in this file:

[Definition]

failregex = ^<HOST> .* \“POST .*/lists/admin.* HTTP/2.0\” 200 3.*$

^<HOST> .* \”GET .*/lists/admin.* HTTP/2.0\” 401 195 .*$

ignoreregex =

The second line in the ‘failregex’ section identifies the failed log in attempts against the HTTP authentication login window, which is displayed right before loading the phpList login page, and can be also subject to brute-force attacks.

Now open the /etc/fail2ban/jail.local file:

nano /etc/fail2ban/jail.local

Add the following section below the [lighttpd-auth] section:

[phplist]

enabled = true

filter = phplist

logpath = /var/log/sites/lists.example.com/access.log

port = 80,443

maxretry = 4

bantime = 259200

Replace example.com with the main domain hosted on your server. Reload Fail2ban:

systemctl reload fail2ban

23.22. Upgrading phpList

Before upgrading phpList to a new version, it’s recommended to verify if the new version has been tested and confirmed to function well within the suite of applications described in this guide. Once we test an application and confirm that it works well, we include it on this page.

As with every application upgrade, before upgrading, you should make a backup copy of the phpList database and files.

First make a backup of your phpList database by logging in to phpMyAdmin, clicking on the phpList database (phplistdb) on the left panel, then going to ‘Export’ > ‘Go’ > ‘Save File’ > ‘OK’. Don’t forget to include the date of the backup in the name of the exported file (Eg.: phplist-2020-9-25.sql).

Then make a backup copy of the whole directory that contains the phpList files:

cd /var/bm_archives

tar czf var-www-lists.example.com-2020-09-25.tar.gz /var/www/lists.example.com

Replace example.com-2020-09-25 with your domain and date of backup. You can use a FTP client like FileZilla to upload the database backup from your local computer (phplist-2020-9-25.sql) to the backups directory on the remote server (/var/bm_archives). Also, you can use FTP to download the backup of phpList files (var-www-lists.example.com-2020-09-25.tar.gz) to your local computer. In this way, you will have the backups both on the remote server and your local computer.

By default, phpList will check for updates every 7 days. When a new version has been released, you will see a notification on the Dashboard saying “A new version of phpList is available!”. At the bottom of that notification there will be a ‘Download’ link. Click on it. It will take you to https://www.phplist.org/download-phplist/ . There click on the ‘Download ZIP’ link to download the latest version of phpList to your computer. After you download it, extract the archive, pick the ‘lists’ directory and upload it via FTP to the /var/www/lists.example.com directory on your server. Since you moved the content of the main phpList configuration file (/var/www/lists.example.com/lists/config/config.php) outside the webroot, when you upload the ‘lists’ directory from your computer to the /var/www/lists.example.com directory, you can choose to overwrite all the files on the remote server. In this way, the content of the configuration file will not be lost.

Next, edit the config.php file like this:

cd /var/www/lists.example.com/lists/config

cat /dev/null > config.php

nano config.php

Add the following line inside this file:

<?php include(‘/srv/scripts/phplist.php’); ?>

Change the ownership and permissions for the /var/www/lists.example.com directory and all its subdirectories:

chown -R www-data:www-data /var/www/lists.example.com

find /var/www/lists.example.com -type d -exec chmod 750 {} +

find /var/www/lists.example.com -type f -exec chmod 640 {} +

Next, log in to your phpList administration interface at:

https://lists.example.com/lists/admin

You will see a big red box at the top of the screen urging you to upgrade the phpList database. Click ‘Upgrade’, then ‘Upgrade’ again. When the process finishes you will be informed that the upgrade succeeded.

You can send your questions and comments to: