diff --git a/action.php b/action.php index f8607fc..d75d0ba 100644 --- a/action.php +++ b/action.php @@ -9,6 +9,9 @@ */ require_once __DIR__ . "/required.php"; +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\Exception; + if ($VARS['action'] !== "signout") { dieifnotloggedin(); } @@ -31,7 +34,97 @@ function returnToSender($msg, $arg = "") { switch ($VARS['action']) { case "sendpub": - die("not implemented yet."); + try { + ini_set('max_execution_time', 60 * 5); + + // Setup mailer + $mail = new PHPMailer(true); + $mail->isSMTP(); + $mail->Host = SMTP_HOST; + $mail->SMTPAuth = SMTP_AUTH; + if (SMTP_AUTH) { + $mail->Username = SMTP_USERNAME; + $mail->Password = SMTP_PASSWORD; + } + if (SMTP_SECURITY != "none") { + $mail->SMTPSecure = SMTP_SECURITY; + } + $mail->Port = SMTP_PORT; + $mail->isHTML(false); + $mail->setFrom(SMTP_FROMADDRESS, SMTP_FROMNAME); + + + // Get addresses + $addresses = []; + if ($database->has('mail_lists', ['listid' => $VARS['list']])) { + $addresses = $database->select("addresses", 'email', ['listid' => $VARS['list']]); + } else { + returnToSender("invalid_listid"); + } + + + // Split address list into batches + $segmented = []; + $s = 0; + for ($i = 0; $i < count($addresses); $i++) { + $segmented[$s][] = $addresses[$i]; + if (count($segmented[$s]) >= SMTP_BATCH_SIZE) { + $s++; + } + } + + + // Build message content + if (empty($VARS['subject']) || trim($VARS['subject']) == "") { + returnToSender("invalid_parameters"); + } + if (empty($VARS['pubid']) || !$database->has("publications", ['pubid' => $VARS['pubid']])) { + returnToSender("invalid_pubid"); + } + + $mail->Subject = $VARS['subject']; + + $parsedown = new Parsedown(); + $parsedown->setSafeMode(true); + $html = $parsedown->text($VARS['message']); + + if (strpos(URL, "https://") === 0 || strpos(URL, "http://") === 0) { + $url = URL; + } else { + // Don't trust the URL setting, it's not an absolute URL + $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; + $path = explode("/", $_SERVER["REQUEST_URI"]); + array_pop($path); + $url .= implode("/", $path); + } + $url = $url . (substr($url, -1) == '/' ? '' : '/'); + $puburl = $url . "view.php?id=" . $VARS['pubid']; + $unsuburl = $url . "unsubscribe.php"; + + $link = "$puburl\n"; + $footer = "
\nUnsubscribe: $unsuburl"; + + $mail->Body = $html . "
\n" . $link . $footer; + $mail->AltBody = $VARS['message'] . "\n" . $puburl . "\n\n-----\nUnsubscribe: $unsuburl"; + + var_dump($mail->Body); + var_dump($mail->AltBody); + + // Send the mail + foreach ($segmented as $segment) { + foreach ($segment as $s) { + $mail->addBCC($s); + } + $mail->send(); + $mail->clearAllRecipients(); + } + } catch (Exception $ex) { + returnToSender("mail_error", $mail->ErrorInfo); + } + + $database->update("publications", ['mailedon' => date("Y-m-d H:i:s"), 'mailedto' => $VARS['list']], ['pubid' => $VARS['pubid']]); + + returnToSender("mail_sent"); break; case "editpub": $insert = true; @@ -51,9 +144,9 @@ switch ($VARS['action']) { returnToSender('invalid_parameters'); } $VARS['columns'] = 4; - /*if (!is_numeric($VARS['columns'])) { - returnToSender('invalid_parameters'); - }*/ + /* if (!is_numeric($VARS['columns'])) { + returnToSender('invalid_parameters'); + } */ if (!preg_match("/([A-Za-z0-9_])+/", $VARS['style'])) { $VARS['style'] = ""; } diff --git a/composer.json b/composer.json index 536b015..f9ab61e 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,9 @@ "type": "project", "require": { "catfan/medoo": "^1.5", - "guzzlehttp/guzzle": "^6.2" + "guzzlehttp/guzzle": "^6.2", + "phpmailer/phpmailer": "^6.0", + "erusev/parsedown": "^1.7" }, "license": "MPL-2.0", "authors": [ diff --git a/composer.lock b/composer.lock index 621b2e3..1a543ae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "924faa0bab19df95b4ae0a73869d1a64", - "content-hash": "78381125cec2264192fbd0feacb704c1", + "hash": "fdb05abdb1063e98e613030e3ef8d688", + "content-hash": "9f22ca23358d7714943dd9e7fe1b22f0", "packages": [ { "name": "catfan/medoo", @@ -66,6 +66,52 @@ ], "time": "2017-12-25 17:02:41" }, + { + "name": "erusev/parsedown", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", + "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "time": "2018-03-08 01:11:30" + }, { "name": "guzzlehttp/guzzle", "version": "6.3.0", @@ -247,6 +293,72 @@ ], "time": "2017-03-20 17:10:46" }, + { + "name": "phpmailer/phpmailer", + "version": "v6.0.5", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "cb3ea134d4d3729e7857737d5f320cce9caf4d32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/cb3ea134d4d3729e7857737d5f320cce9caf4d32", + "reference": "cb3ea134d4d3729e7857737d5f320cce9caf4d32", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "doctrine/annotations": "1.2.*", + "friendsofphp/php-cs-fixer": "^2.2", + "phpdocumentor/phpdocumentor": "2.*", + "phpunit/phpunit": "^4.8 || ^5.7", + "zendframework/zend-eventmanager": "3.0.*", + "zendframework/zend-i18n": "2.7.3", + "zendframework/zend-serializer": "2.7.*" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "time": "2018-03-27 13:49:45" + }, { "name": "psr/http-message", "version": "1.0.1", diff --git a/database.mwb b/database.mwb index 0449ae7..e859501 100644 Binary files a/database.mwb and b/database.mwb differ diff --git a/lang/en_us.php b/lang/en_us.php index 3e4720f..2e0a0cf 100644 --- a/lang/en_us.php +++ b/lang/en_us.php @@ -77,6 +77,7 @@ define("STRINGS", [ "view file" => "View File", "password incorrect" => "Password incorrect.", "invalid listid" => "Invalid list ID.", + "new list" => "New List", "list saved" => "Mailing list saved.", "list deleted" => "Mailing list deleted.", "adding list" => "Adding mailing list", @@ -97,5 +98,8 @@ define("STRINGS", [ "placeholder subject" => "Type an email subject", "message" => "Message", "default message" => "Hello, \nClick the link to view the newsletter:", - "cancel" => "Cancel" + "cancel" => "Cancel", + "mail error {arg}" => "Mail error: {arg}", + "mail sent" => "Mail sent!", + "last mailed on x to y" => "Publication last mailed on {x} to list {y}.", ]); diff --git a/lang/messages.php b/lang/messages.php index 40d27b8..a7ca5c2 100644 --- a/lang/messages.php +++ b/lang/messages.php @@ -45,4 +45,12 @@ define("MESSAGES", [ "string" => "list deleted", "type" => "success" ], + "mail_error" => [ + "string" => "mail error {arg}", + "type" => "danger" + ], + "mail_sent" => [ + "string" => "mail sent", + "type" => "success" + ], ]); diff --git a/pages/send.php b/pages/send.php index b5eb16b..0f5969d 100644 --- a/pages/send.php +++ b/pages/send.php @@ -13,14 +13,40 @@ if (is_empty($VARS['pubid']) || !$database->has("publications", ['pubid' => $VAR } $lists = $database->select("mail_lists", ['listid', 'listname']); + +$lastmailed = $database->get("publications", ["[>]mail_lists" => ["mailedto" => "listid"]], ['mailedon', 'mailedto', 'listname'], ['pubid' => $VARS['pubid']]); + +if (strpos(URL, "https://") === 0 || strpos(URL, "http://") === 0) { + $url = URL; +} else { + // Don't trust the URL setting, it's not an absolute URL + $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; + $path = explode("/", $_SERVER["REQUEST_URI"]); + array_pop($path); + $url .= implode("/", $path); +} +$url = $url . (substr($url, -1) == '/' ? '' : '/'); ?> -
+

+ +
+ date(DATETIME_FORMAT, strtotime($lastmailed['mailedon'])), + "y" => $lastmailed['listname']]); + ?> +
+
@@ -40,12 +66,12 @@ $lists = $database->select("mail_lists", ['listid', 'listname']);
- ", lang("default message", false)); ?> +", lang("default message", false)); ?>
- /view.php?id= + view.php?id=
- Unsubscribe: /unsubscribe.php?a=xxxxx@example.com + Unsubscribe: unsubscribe.php
@@ -63,16 +89,16 @@ $lists = $database->select("mail_lists", ['listid', 'listname']);
+ echo htmlspecialchars($VARS['pubid']); + ?>" />
\ No newline at end of file diff --git a/settings.template.php b/settings.template.php index 1b2ebf9..8edaa0b 100644 --- a/settings.template.php +++ b/settings.template.php @@ -20,6 +20,15 @@ define("DB_CHARSET", "utf8"); // Name of the app. define("SITE_TITLE", "NewsPen"); +define("SMTP_HOST", ""); +define("SMTP_AUTH", true); +define("SMTP_SECURITY", "tls"); // tls, ssl, or none +define("SMTP_PORT", 25); +define("SMTP_USERNAME", ""); +define("SMTP_PASSWORD", ""); +define("SMTP_FROMADDRESS", "newspen@example.com"); +define("SMTP_FROMNAME", "NewsPen"); +define("SMTP_BATCH_SIZE", 20); // URL of the AccountHub API endpoint define("PORTAL_API", "http://localhost/accounthub/api.php"); diff --git a/static/js/send.js b/static/js/send.js index 3b7d77f..f98e12e 100644 --- a/static/js/send.js +++ b/static/js/send.js @@ -11,4 +11,12 @@ $("#subject").on("keyup", function () { $("#message").on("keyup", function () { $("#messagepreview").html(snarkdown($("#message").val())); +}); + +$("#sendform").submit(function () { + $("#sendbtn").attr("disabled", true); + $("#sendbtn").prop("disabled", true); + $("#cancelbtn").attr("disabled", true); + $("#cancelbtn").prop("disabled", true); + $("#sendbtn").html(" Sending..."); }); \ No newline at end of file diff --git a/unsubscribe.php b/unsubscribe.php index 5435ac8..02b74b5 100644 --- a/unsubscribe.php +++ b/unsubscribe.php @@ -8,22 +8,38 @@ require __DIR__ . "/required.php"; require __DIR__ . "/lib/iputils.php"; -$address = $VARS['a']; +?> + +Unsubscribe + +"; + $address = $VARS['a']; -if (!filter_var($address, FILTER_VALIDATE_EMAIL)) { - die("Invalid email address."); -} + engageRateLimit(); -$address = str_replace("%", '\%', $address); + if (!filter_var($address, FILTER_VALIDATE_EMAIL)) { + die("Invalid email address."); + } -echo $address; + $address = str_replace("%", '\%', $address); -if ($database->has('addresses', ['email' => $address])) { - $count = $database->count('addresses', ['email' => $address]); - $database->delete('addresses', ['email' => $address]); - die("$address has been removed from $count mailing " . ($count === 1 ? "list" : "lists") . "."); + if ($database->has('addresses', ['email' => $address])) { + $count = $database->count('addresses', ['email' => $address]); + $database->delete('addresses', ['email' => $address]); + die("$address has been removed from $count mailing " . ($count === 1 ? "list" : "lists") . "."); + } else { + die("$address has already been removed."); + } } else { - die("$address has already been removed."); + ?> +

Enter your email address below to unsubscribe:

+
+ +
+ +
+