Initial Addition of Code

master
Dubtempo 2021-08-11 20:39:28 -07:00
parent 232276d691
commit 87841978e2
11 changed files with 641 additions and 26 deletions

54
.gitignore vendored
View File

@ -1,3 +1,5 @@
config.ini
# ---> Windows
# Windows thumbnail cache files
Thumbs.db
@ -35,30 +37,30 @@ $RECYCLE.BIN/
.nfs*
# ---> macOS
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

47
ImapMailMerge.php Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/php
<?php
/**
* Program Name: DubTempo's Mail Merge Service for Netpay Payroll
* Program URI: https://gitea.dubtempo.com/NPPR/ImapMailMerge
* Author: Dubtempo
* Author URI: http://dubtempo.com
* License: Copyright Dubtempo
* Version: 1.0.1
* @SuppressWarnings(PHPMD.ElseExpression)
* Description: Netpay employees were blacklisted from all ATT&T services for
* the practice of sending reminders to clients. They would send
* to & from themselves, with BCCs for each client to be reminded.
*
* This code helps replace that practice with direct messages.
* To use, approved users send their notifications to
* <mailmerge@netpaypayroll.com>, with the destination clients
* specified via Return-To Headers.
*
* Note!!!:
* Each user must be whitelisted in IMAPMailMerge->safelist.
*/
//error_reporting(-1);
//ini_set('display_errors', 'On');
//set_error_handler("var_dump");
//ini_set("mail.log", "/home4/dubtempo/custom_scripts/imapmailmerge/mail.log");
//ini_set("mail.add_x_header", TRUE);
$GoMailMerge = new MailMerge();
class MailMerge
{
public function __construct()
{
require_once __DIR__ . '/autoload.php';
$imap_connection = new \DT\ImapMailMerge\Imap\Connection;
$process_safelist = new DT\ImapMailMerge\Main\ProcessSafelist;
$process_inbox = new DT\ImapMailMerge\Main\ProcessInbox;
$process_messages = new DT\ImapMailMerge\Main\ProcessMessages;
}
}

39
autoload.php Executable file
View File

@ -0,0 +1,39 @@
<?php
/*
* If the $class being instantiated is within the namespace `Acme\ExampleProject`, then find the correct
* file within our `src` directory
* https://gist.github.com/mhull/61832ecd957821c8912b446e98ddcfde
* https://resoundingechoes.net/development/autoloading-classes-php/
*/
spl_autoload_register( function( $class ) {
# namespace prefix that we will use for autoloading the appropriate classes and ignoring others
$prefix = 'DT\\ImapMailMerge\\';
# base directory where our project's files reside
$base_dir = __DIR__ . '/src/';
/**
* Does the class being called use our specific namespace prefix?
*
* - Compare the first {$len} characters of the class name against our prefix
* - If no match, move to the next registered autoloader in the system (if any)
*/
# character length of our prefix
$len = strlen( $prefix );
# if the first {$len} characters don't match
if ( strncmp( $prefix, $class, $len ) !== 0 ) {
return;
}
# get the name of the class after our prefix has been peeled off
$class_name = str_replace( $prefix, '', $class );
/**
* Perform normalizing operations on the namespace/class string in order to get the file name to be required
*
* - Replace namespace separators with directory separators in the class name
* - Prepend the base directory
* - Append with .php
*/
$file = $base_dir . str_replace('\\', '/', $class_name ) . '.php';
# require the file if it exists
if( file_exists( $file ) ) {
require $file;
}
}); # end: spl_autoload_register()

18
config/config.ini.example Normal file
View File

@ -0,0 +1,18 @@
;[branding]
organization = ""
;[safe_list]
safelist[] = "User1@Example.com";
safelist[] = "User2@Example.com";
;[imap_accounts]
imapServer="{localhost:993/imap/ssl}";
scriptEmail="MailMaster@Example.com";
adminEmail="Admin@Example.com";
mainEmail="mailmerge@example.com";
mainEmailPassword="fancy";
;[settings]
limitRecipients=30;

49
src/Imap/Connection.php Executable file
View File

@ -0,0 +1,49 @@
<?php
namespace DT\ImapMailMerge\Imap;
class Connection
{
private $config;
private $sender;
public static $imapConn;
public function __construct()
{
$this->config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
$this->sender = new \DT\ImapMailMerge\Imap\Sender;
$this->imapConnection();
}
private function imapConnection()
{
if (
self::$imapConn = imap_open(
$this->config['imapServer'] . "INBOX",
$this->config['mainEmail'],
$this->config['mainEmailPassword']
)
) {
// silence on success
// this can be uncommented for testing
/*
$this->sender->imapEmailLog(
$this->config['adminEmail'],
"IMAPMailMerge: Connected to " . $this->config['mainEmail'] . "!",
"No issue there!"
);
*/
} else {
$this->sender->imapEmailLog(
$this->config['adminEmail'],
"IMAPMailMerge: Couldn't Connect to " . $this->config['mainEmail'] . "!",
"Issue with the imap connection. Details:" . PHP_EOL . PHP_EOL . json_encode(imap_errors()) . PHP_EOL . PHP_EOL
);
die();
}
}
}

31
src/Imap/Folders.php Executable file
View File

@ -0,0 +1,31 @@
<?php
namespace DT\ImapMailMerge\Imap;
class Folders
{
private $config;
private $imap;
public function __construct()
{
$this->config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
$this->imap = new \DT\ImapMailMerge\Imap\Connection;
}
public function imapDir($dir)
{
$mbox_pre = $this->imap;
$mbox = $mbox_pre::$imapConn;
imap_reopen(
$mbox,
"" . $this->config['imapServer'] . "$dir"
);
}
public function imapCopyToSent() {
// this would be a cool feature to add
}
}

53
src/Imap/Sender.php Executable file
View File

@ -0,0 +1,53 @@
<?php
namespace DT\ImapMailMerge\Imap;
class Sender
{
private static $config;
public static $imapConn;
public function __construct()
{
self::$config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
}
public function imapEmailLog($to, $subject, $body)
{
$headers = "Bcc: " . self::$config['adminEmail'] . PHP_EOL;
$this->imapSend( self::$config['scriptEmail'], $to, $subject, $body, $headers );
}
public function imapSend($from, $to, $subject, $body, $headers)
{
// prevent user/script details being exposed in X-PHP-Script header
$oldphpself = (isset($SERVER['PHP_SELF'])) ? $SERVER['PHP_SELF'] : "";
$oldremoteaddr = (isset($SERVER['REMOTE_ADDR'])) ? $SERVER['REMOTE_ADDR'] : "";
if (!empty($oldphpself) || !empty($oldremoteaddr)) {
unset($SERVER['PHP_SELF']);
unset($SERVER['REMOTE_ADDR']);
}
// send the email
$sendmail = mail($to, $subject, $body, $headers, "-f $from");
// restore obfuscated server variables
$SERVER['PHP_SELF'] = $oldphpself;
$SERVER['REMOTE_ADDR'] = $oldremoteaddr;
// process mail return
if ($sendmail == true) {
//echo "Message sent successfully to $to...";
return '*status=Sent*:' . $to;
} else {
//echo "Message could not be sent to $to...";
//print_r(error_get_last());
return '*status=Not Sent*:' . $to;
}
}
}

70
src/Imap/Stringer.php Executable file
View File

@ -0,0 +1,70 @@
<?php
namespace DT\ImapMailMerge\Imap;
class Stringer
{
public function __construct()
{
// nothing needed
}
public function sanitizeEmailAddresses($toClients) {
if (empty($toClients)) {
return '';
} else {
// search for commas within ""'s
$toClientsSafeCommas = preg_replace('/("[^,]*)(,|.)(.*")/mi', '$1 $3', $toClients);
// strip other potential bogies
$toClientsSafeChars = preg_replace('/\\\|\/|\"|\'/mi', '', $toClientsSafeCommas);
// explode on commas separating addresses
$safeClientsArray = explode(',', $toClientsSafeChars);
}
return $safeClientsArray;
}
public function createMessageInfoString($header, $template, $safeReturn)
{
$returnStr = "";
$returnStr .= "Subject:" . PHP_EOL . " " . $template->subject . PHP_EOL . PHP_EOL . "From:" . PHP_EOL . " " . $template->accountAddress . PHP_EOL . PHP_EOL . "To:" . PHP_EOL . " ";
$itr = 1;
if ($safeReturn) {
$results = explode('*status=', $safeReturn);
foreach ($results as $result) {
if(empty($result)) continue;
$resultSplit = explode('*:', $result);
$resultValue = $resultSplit[0];
$addressValue = $resultSplit[1];
$returnStr .= "#" . $itr . ": [" . $resultValue . "] " . $addressValue . PHP_EOL . PHP_EOL . " ";
$itr++;
}
}
$returnStr .= PHP_EOL . "*** NOTE *** : Please check your inbox for any bounced (ie: Return to Sender) emails. If an address you entered was not valid, you will see such a message and will need to resend your message to that client." . PHP_EOL;
return $returnStr;
}
public function createMessageInfoStringOld($header, $template, $toClients)
{
return "Subject:" . PHP_EOL . " " . $template->subject . PHP_EOL . PHP_EOL
. "From:" . PHP_EOL . " " . $template->accountAddress . PHP_EOL . PHP_EOL
. "To:" . PHP_EOL . " " . $this->createReplyToMessageString($toClients) . PHP_EOL . PHP_EOL;
//. "Body:" . PHP_EOL . " " . $template->body . PHP_EOL . PHP_EOL;
}
public function createReplyToMessageString($toClients = "")
{
$returnStr = "";
$itr = 1;
if ($toClients) {
foreach ($toClients as $addressKey => $addressValue) {
$returnStr .= "#" . $itr . ": " . $addressValue . PHP_EOL . PHP_EOL . " ";
$itr++;
}
}
return $returnStr;
}
}

48
src/Main/ProcessInbox.php Executable file
View File

@ -0,0 +1,48 @@
<?php
namespace DT\ImapMailMerge\Main;
class ProcessInbox
{
private $config;
public $imap;
public $folders;
public function __construct()
{
$this->config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
$this->imap = new \DT\ImapMailMerge\Imap\Connection;
$this->folders = new \DT\ImapMailMerge\Imap\Folders;
$this->imapProcessInbox();
}
public function imapProcessInbox()
{
$this->folders->imapDir('INBOX');
$mbox_pre = $this->imap;
$mbox = $mbox_pre::$imapConn;
$numMessages = imap_num_msg($mbox);
for ($i = $numMessages; $i > 0; $i--) {
$msgId = imap_uid($mbox, $i);
$header = (imap_header($mbox, $i)) ? imap_header($mbox, $i) : "";
$account = (isset($header->from[0])) ? $header->from[0] : "";
$accountSlug = strtolower($account->mailbox);
if (in_array($accountSlug, $this->config['safelist'])) {
imap_mail_move($mbox, $msgId, 'INBOX.' . $accountSlug, CP_UID);
imap_expunge($mbox);
}
}
imap_expunge($mbox);
}
}

191
src/Main/ProcessMessages.php Executable file
View File

@ -0,0 +1,191 @@
<?php
namespace DT\ImapMailMerge\Main;
class ProcessMessages
{
private $config;
public $imap;
public $folders;
public $stringer;
public function __construct()
{
$this->config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
$this->imap = new \DT\ImapMailMerge\Imap\Connection;
$this->sender = new \DT\ImapMailMerge\Imap\Sender;
$this->folders = new \DT\ImapMailMerge\Imap\Folders;
$this->stringer = new \DT\ImapMailMerge\Imap\Stringer;
$this->imapProcessMessages();
}
public function imapProcessMessages()
{
for ($i = count($this->config['safelist']); $i > 0; $i--) {
$accountSlug = $this->config['safelist'][$i-1];
$accountFolder = 'INBOX.' . $accountSlug;
$this->folders->imapDir($accountFolder);
$mbox_pre = $this->imap;
$mbox = $mbox_pre::$imapConn;
$numMessages = imap_num_msg($mbox);
if (($numMessages) === 0) {
continue;
}
for ($i = $numMessages; $i > 0; $i--) {
$msgId = imap_uid($mbox, $i);
$header = (imap_header($mbox, $i)) ? imap_header($mbox, $i) : "";
$structure = (imap_fetchstructure($mbox, $i)) ? imap_fetchstructure($mbox, $i) : "";
$account = (isset($header->from[0])) ? $header->from[0] : "";
$toClients = $this->stringer->sanitizeEmailAddresses($header->reply_toaddress);
$accountSafeName = (isset($account->personal)) ? $account->personal : "";
$template = new \StdClass();
$template->{"header"} = (isset($header)) ? $header : "";
$template->{"body"} = (imap_body($mbox, $i)) ? imap_body($mbox, $i) : "";
$template->{"body_text"} = imap_fetchbody($mbox, $i, 1);
$template->{"body_html"} = imap_fetchbody($mbox, $i, 2);
$template->{"subject"} = (isset($header->subject)) ? $header->subject : "";
$template->{"accountFrom"} = imap_rfc822_write_address($account->mailbox, $account->host, $accountSafeName);
$template->{"accountAddress"} = $account->mailbox . '@' . $account->host;
// get boundary from header structure
$params = $structure->parameters;
foreach ($params as $itr => $keys) {
foreach ($keys as $key => $value) {
if ($key === 'attribute' && $value === 'boundary') {
$template->{"boundary"} = $keys->value;
}
}
}
// get plain text and html charsets
$headers = "Reply-To: $template->accountAddress" . PHP_EOL;
$headers .= "From: $template->accountFrom" . PHP_EOL;
$headers .= "Organization: " . $this->config['organization'] . PHP_EOL;
$headers .= "MIME-Version: 1.0" . PHP_EOL;
$headers .= "Content-Transfer-Encoding: binary" . PHP_EOL;
//$headers .= "Return-Path: <" . $template->accountAddress . ">" . PHP_EOL;
//$headers .= "X-PHP-Script: " . PHP_EOL;
$headers .= "X-Mailer: PHP". phpversion() . PHP_EOL;
$headers .= "X-Priority: 1". PHP_EOL;
$headers .= "X-MSMail-Priority: Normal". PHP_EOL;
$headers .= "Importance: Normal". PHP_EOL;
if ($template->body_html) {
$headers .= "Content-Type: multipart/alternative; charset=ISO-8859-1; boundary=" . $template->boundary . PHP_EOL;
} elseif ($template->body_text) {
$headers .= "Content-Type: text/plain; charset=US-ASCII" . PHP_EOL;
$headers .= "Content-Type: text/plain; charset=US-ASCII" . PHP_EOL;
$headers .= "Content-Transfer-Encoding: 7bit" . PHP_EOL;
}
// test if no Reply To was set, if so, move to error and continue
$replyToEmpty = false;
if (empty($toClients)) {
$replyToEmpty = true;
}
// break if attempting to send to more than the max limit of recipients
if (count($toClients) > $this->config['limitRecipients']) {
imap_mail_move($mbox, $msgId, 'INBOX.'.$accountSlug.'.error', CP_UID);
imap_expunge($mbox);
$this->sender->imapEmailLog(
$template->accountFrom,
"IMAPMailMerge: Error!",
"Too Many Recipients in MailMerge Message" . PHP_EOL . PHP_EOL . PHP_EOL
. $this->stringer->createMessageInfoString($header, $template, $toClients)
);
continue;
}
if ($replyToEmpty) {
imap_mail_move($mbox, $msgId, 'INBOX.'.$accountSlug.'.error', CP_UID);
imap_expunge($mbox);
$this->sender->imapEmailLog(
$template->accountFrom,
"IMAPMailMerge: Error!",
"No Return-To In MailMerge Message" . PHP_EOL . PHP_EOL . PHP_EOL
. $this->stringer->createMessageInfoString($header, $template, $toClients)
);
continue;
}
// compose mail for each Reply To
$safeReturn = '';
foreach ($toClients as $replytoaddr) {
$template->{"clientTo"} = $replytoaddr;
// below not used ???
//$recipient = (isset($replytoaddr)) ? imap_rfc822_parse_adrlist($replytoaddr, 'netpaypayroll.com') : "";
//$accountSafeName = (isset($recipient->personal)) ? $recipient->personal : "";
// send email
$safeReturn .= $this->sender->imapSend(
$template->accountAddress,
$template->clientTo,
$template->subject,
$template->body,
$headers
);
}
// move email
if (strpos($safeReturn, 'Not Sent')) {
imap_mail_move($mbox, $msgId, 'INBOX.'.$accountSlug.'.fail', CP_UID);
imap_expunge($mbox);
$this->sender->imapEmailLog(
$template->accountFrom,
"IMAPMailMerge: Fail!",
"Problem Sending MailMerge Message" . PHP_EOL . PHP_EOL . PHP_EOL
. $this->stringer->createMessageInfoString($header, $template, $safeReturn)
);
} elseif (strpos($safeReturn, 'Sent')) {
imap_mail_move($mbox, $msgId, 'INBOX.'.$accountSlug.'.success', CP_UID);
imap_expunge($mbox);
$this->sender->imapEmailLog(
$template->accountFrom,
"IMAPMailMerge: Success!",
"Sucessfully Sent MailMerge Message" . PHP_EOL . PHP_EOL . PHP_EOL
. $this->stringer->createMessageInfoString($header, $template, $safeReturn)
);
} else {
imap_mail_move($mbox, $msgId, 'INBOX.'.$accountSlug.'.error', CP_UID);
imap_expunge($mbox);
$this->sender->imapEmailLog(
$template->accountFrom,
"IMAPMailMerge: Error!",
"Problem Sending MailMerge Message" . PHP_EOL . PHP_EOL . PHP_EOL
. $this->stringer->createMessageInfoString($header, $template, $safeReturn)
);
}
}
}
}
}

67
src/Main/ProcessSafelist.php Executable file
View File

@ -0,0 +1,67 @@
<?php
namespace DT\ImapMailMerge\Main;
/**
* Testing DocBlock
* @var [private] $config
* @var [private] $sender
* @var [public] $imap
*/
class ProcessSafelist
{
private $config;
private $sender;
public $imap;
public function __construct()
{
$this->config = parse_ini_file( dirname(dirname(dirname(__FILE__))) . "/config/config.ini", true);
$this->sender = new \DT\ImapMailMerge\Imap\Sender;
$this->imap = new \DT\ImapMailMerge\Imap\Connection;
$this->imapProcessSafelist();
}
/**
* [imapProcessSafelist description]
* @return [type] [description]
*/
public function imapProcessSafelist()
{
$mbox_pre = $this->imap;
$mbox = $mbox_pre::$imapConn;
$dirs = imap_list($mbox, $this->config['imapServer'], "*");
for ($i = count($this->config['safelist']); $i > 0; $i--) {
$accountSlug = $this->config['safelist'][$i-1];
$accountDir = $this->config['imapServer'].'INBOX.'.$accountSlug;
$accountError = $this->config['imapServer'].'INBOX.'.$accountSlug.'.error';
$accountSuccess = $this->config['imapServer'].'INBOX.'.$accountSlug.'.success';
$accountFail = $this->config['imapServer'].'INBOX.'.$accountSlug.'.fail';
$accountDirs = array($accountDir, $accountError, $accountSuccess, $accountFail);
foreach ($accountDirs as $dir) {
if (array_search($dir, $dirs) === false) {
if (! imap_createmailbox($mbox, $dir)) {
$this->sender->imapEmailLog(
$this->config['adminEmail'],
"IMAPMailMerge: Process SafeList Errored!",
"Issue with the imap directories."
);
} else {
imap_subscribe($mbox, $dir);
}
}
}
}
}
}