We will build an Email Marketing platform by using a VPS. I took a minimul VPS 2 cores, 2 gb ram, 80 gb nvme space.

Here, in this tutorial, my OS is Debian 12 with DirectAdmin

Here, the example IP address of the vps is 23.220.75.245

First, check the IP address is not in blacklist and not in poor sending reputation !

https://www.talosintelligence.com/reputation_center/lookup?search=23.220.75.245

If the IP reputation is bad, please change your vps IP address.

 

Let’s start building the system;

1) Initialize Debian

 

2) Install DirectAdmin

 

3) Configure DirectAdmin and Tweak/php Settings

 

4) Add Hostname DNS

 

5) Add Domain and Create Email Addresses

 

6) Ensure Email Deliverability Checklist

 

7) Install phpList

 

8) Install phpList Plugins

 

9) Configure phpList

 

 

Configure Bounce Rules; GUI > Config > Bounce Rules

Regular Expression:
user unknown|no such user|mailbox unavailable|no mailbox|recipient not found|does not exist|is inactive|is disabled
Action: Delete subscriber and bounce
Memo: Hard bounce – invalid recipient / mailbox doesn’t exist.

Regular Expression:
mailbox full|out of storage|quota exceeded|over quota
Action: Unconfirm subscriber and delete bounce
Memo: Mailbox full – temporary failure.

Regular Expression:
broken pipe|closed connection|connection timed out|no smtp service|unrouteable address
Action: Delete subscriber and bounce
Memo: Invalid connection – remove address.

Regular Expression:
host or domain name not found|domain does not exist|no such domain
Action: Delete subscriber and bounce
Memo: Invalid domain – remove address.

 

10) Configure Attachment File Types

File Manager: > /public_html/admin/send_core.php

After the line (maybe line number 280): e.g: $newtmpfile = $remotename.time();
Before the line (maybe line number 281): e.g: move_uploaded_file($tmpfile, $GLOBALS['tmpdir'].'/'.$newtmpfile);

Put the below codes between the above two lines;

 

// ==== CUSTOM: Attachments File extension validation start ====
if (isset($_FILES[$fieldname]) && is_array($_FILES[$fieldname]) && !empty($_FILES[$fieldname]['name'])) {

$allowed_ext = ['pdf','docx','xlsx','pptx','jpg','jpeg','png','gif','txt','mp3','mp4'];
$ext = strtolower(pathinfo($_FILES[$fieldname]['name'], PATHINFO_EXTENSION));

if (!in_array($ext, $allowed_ext)) {
$msg = '<div style="max-width:520px;margin:60px auto;padding:25px;
border:2px solid #d33;border-radius:12px;background:#fff5f5;color:#d33;
font-family:Segoe UI,Arial,sans-serif;text-align:center;box-shadow:0 2px 10px rgba(0,0,0,0.1);">
<h2 style="margin-top:0;">🚫 Invalid File Type!</h2>
<p>Only the following file types are allowed:</p>
<p style="font-weight:bold;">pdf, docx, xlsx, pptx, jpg, jpeg, png, gif, txt, mp3, mp4</p>
<a href="javascript:history.back()" style="display:inline-block;margin-top:12px;
background:#d33;color:#fff;padding:10px 18px;border-radius:6px;
text-decoration:none;font-weight:500;">⬅ Go Back</a>
</div>';

if (ob_get_length()) {
@ob_clean();
}

echo $msg;
@flush();

if (function_exists('printFooter')) {
@printFooter();
}

exit;
}
}
// ==== CUSTOM: Attachments File extension validation end ====

 

File Manager: > /public_html/admin/plugins/TinyMCEPlugin/connector_phplist.php

Where the line starts with: $opts = array(

Replace with the below codes for the whole $opts section;

$opts = array(
'debug' => false,
'roots' => array(
array(
'driver' => 'LocalFileSystem',
'path' => $uploadPath,
'URL' => $uploadDir,
'accessControl' => 'access',

// ===== Custom upload restrictions =====
'uploadDeny' => array('all'),
'uploadAllow' => array(
'image/png',
'image/jpeg',
'image/gif',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'audio/mpeg',
'video/mp4'
),
'uploadOrder' => array('deny', 'allow'),

// Prevent executable files from being visible or editable
'attributes' => array(
array(
'pattern' => '/\.(php|pl|py|sh|cgi|exe|bat|js|html?)$/i',
'read' => false,
'write' => false,
'locked' => true,
'hidden' => true
)
)
// ===== End custom upload restrictions =====
)
)
);

 

 

11) Configure DirectAdmin Cron Jobs and Cleanup Job

 

Create an Campaign Run Cron Job via DirectAdmin GUI:  *  * * * *

php -q /home/admin/domains/newsletter.mybusinessdomain.com/public_html/admin/index.php -c /home/admin/domains/newsletter.mybusinessdomain.com/public_html/config/config.php -p processqueue >/dev/null 2>&1

 

Create an Invalid Address Delete Cron Job via DirectAdmin GUI:  */10   *  * * *

php -q /home/admin/domains/newsletter.mybusinessdomain.com/public_html/admin/index.php -c /home/admin/domains/newsletter.mybusinessdomain.com/public_html/config/config.php -p processbounces >/dev/null 2>&1

 

Adjust some variables for Exim;

 

nano /etc/exim.variables.conf.custom


message_size_limit=25M
remote_max_parallel=5
queue_only_load=2
deliver_queue_load_max=3
queue_run_max=10

 

da build exim_conf

systemctl restart exim

 

Create a Cleanup Cron Job via PuTTy as root:

crontab -e

*/10 * * * * /usr/sbin/exiqgrep -o 600 -i | xargs /usr/sbin/exim -Mrm >/dev/null 2>&1

 

reboot

 

da build all

 

Wait, until the all build process finished !

 

Optional: If any old queues stuck in exim, clear existing Queue Process and restart the server

 

service exim stop

rm -rf /var/spool/exim/input/*

rm -rf /var/spool/exim/msglog/*

service exim start

 

reboot

 

Leave A Comment