Contact Mails

This commit is contained in:
Roman 2021-04-09 16:05:36 +02:00
parent 2ae209f53b
commit 779550cab4
17 changed files with 324 additions and 59 deletions

24
cli.php

@ -1,6 +1,7 @@
<?php <?php
include_once 'core/core.php'; include_once 'core/core.php';
require_once 'core/datetime.php';
include_once 'core/constants.php'; include_once 'core/constants.php';
use Configuration\Configuration; use Configuration\Configuration;
@ -459,6 +460,26 @@ function onTest($argv) {
} }
function onMail($argv) {
$action = $argv[2] ?? null;
if ($action === "sync") {
$user = getUser() or die();
if (!$user->getConfiguration()->getSettings()->isMailEnabled()) {
_exit("Mails are not configured yet.");
}
$req = new Api\Mail\Sync($user);
printLine("Syncing emails…");
if (!$req->execute()) {
_exit("Error syncing mails: " . $req->getLastError());
}
_exit("Done.");
} else {
_exit("Usage: cli.php mail <sync> [options...]");
}
}
$argv = $_SERVER['argv']; $argv = $_SERVER['argv'];
if (count($argv) < 2) { if (count($argv) < 2) {
_exit("Usage: cli.php <db|routes|settings|maintenance> [options...]"); _exit("Usage: cli.php <db|routes|settings|maintenance> [options...]");
@ -481,6 +502,9 @@ switch ($command) {
case 'test': case 'test':
onTest($argv); onTest($argv);
break; break;
case 'mail':
onMail($argv);
break;
default: default:
printLine("Unknown command '$command'"); printLine("Unknown command '$command'");
printLine(); printLine();

@ -18,6 +18,7 @@ namespace Api\Contact {
private int $notificationId; private int $notificationId;
private int $contactRequestId; private int $contactRequestId;
private ?string $messageId;
public function __construct(User $user, bool $externalCall = false) { public function __construct(User $user, bool $externalCall = false) {
$parameters = array( $parameters = array(
@ -31,6 +32,7 @@ namespace Api\Contact {
$parameters["captcha"] = new StringType("captcha"); $parameters["captcha"] = new StringType("captcha");
} }
$this->messageId = null;
parent::__construct($user, $externalCall, $parameters); parent::__construct($user, $externalCall, $parameters);
} }
@ -48,15 +50,28 @@ namespace Api\Contact {
} }
} }
if (!$this->insertContactRequest()) { $sendMail = $this->sendMail();
return false; $mailError = $this->getLastError();
$insertDB = $this->insertContactRequest();
$dbError = $this->getLastError();
// Create a log entry
if (!$sendMail || $mailError) {
$message = "Error processing contact request.";
if (!$sendMail) {
$message .= " Mail: $mailError";
} }
$this->createNotification(); if (!$insertDB) {
$this->sendMail(); $message .= " Mail: $dbError";
}
if (!$this->success) { error_log($message);
return $this->createError("The contact request was saved, but the server was unable to create a notification."); }
if (!$sendMail && !$insertDB) {
return $this->createError("The contact request could not be sent. The Administrator is already informed. Please try again later.");
} }
return $this->success; return $this->success;
@ -67,9 +82,10 @@ namespace Api\Contact {
$name = $this->getParam("fromName"); $name = $this->getParam("fromName");
$email = $this->getParam("fromEmail"); $email = $this->getParam("fromEmail");
$message = $this->getParam("message"); $message = $this->getParam("message");
$messageId = $this->messageId ?? null;
$res = $sql->insert("ContactRequest", array("from_name", "from_email", "message")) $res = $sql->insert("ContactRequest", array("from_name", "from_email", "message", "messageId"))
->addRow($name, $email, $message) ->addRow($name, $email, $message, $messageId)
->returning("uid") ->returning("uid")
->execute(); ->execute();
@ -112,15 +128,24 @@ namespace Api\Contact {
return $this->success; return $this->success;
} }
private function sendMail() { private function sendMail(): bool {
/*$email = $this->getParam("fromEmail"); $name = $this->getParam("fromName");
$settings = $this->user->getConfiguration()->getSettings(); $email = $this->getParam("fromEmail");
$message = $this->getParam("message");
$request = new \Api\Mail\Send($this->user); $request = new \Api\Mail\Send($this->user);
$this->success = $request->execute(array( $this->success = $request->execute(array(
"to" => $settings->get, "subject" => "Contact Request",
"subject" => "[$siteName] Account Invitation", "body" => $message,
"body" => $messageBody "replyTo" => $email,
));*/ "replyName" => $name
));
if ($this->success) {
$this->messageId = $request->getResult()["messageId"];
}
return $this->success;
} }
} }

@ -1,8 +1,35 @@
<?php <?php
namespace Api { namespace Api {
abstract class MailAPI extends Request {
use Objects\ConnectionData;
abstract class MailAPI extends Request {
protected function getMailConfig(): ?ConnectionData {
$req = new \Api\Settings\Get($this->user);
$this->success = $req->execute(array("key" => "^mail_"));
$this->lastError = $req->getLastError();
if ($this->success) {
$settings = $req->getResult()["settings"];
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
$this->createError("Mail is not configured yet.");
return null;
}
$host = $settings["mail_host"] ?? "localhost";
$port = intval($settings["mail_port"] ?? "25");
$login = $settings["mail_username"] ?? "";
$password = $settings["mail_password"] ?? "";
$connectionData = new ConnectionData($host, $port, $login, $password);
$connectionData->setProperty("from", $settings["mail_from"] ?? "");
$connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? "");
return $connectionData;
}
return null;
}
} }
} }
@ -11,9 +38,11 @@ namespace Api\Mail {
use Api\MailAPI; use Api\MailAPI;
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Strategy\UpdateStrategy;
use External\PHPMailer\Exception; use External\PHPMailer\Exception;
use External\PHPMailer\PHPMailer; use External\PHPMailer\PHPMailer;
use Objects\ConnectionData;
use Objects\User; use Objects\User;
class Test extends MailAPI { class Test extends MailAPI {
@ -45,38 +74,15 @@ namespace Api\Mail {
class Send extends MailAPI { class Send extends MailAPI {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($user, $externalCall, array(
'to' => new Parameter('to', Parameter::TYPE_EMAIL), 'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null),
'subject' => new StringType('subject', -1), 'subject' => new StringType('subject', -1),
'body' => new StringType('body', -1), 'body' => new StringType('body', -1),
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
'replyName' => new StringType('replyName', 32, true, "")
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
private function getMailConfig() : ?ConnectionData {
$req = new \Api\Settings\Get($this->user);
$this->success = $req->execute(array("key" => "^mail_"));
$this->lastError = $req->getLastError();
if ($this->success) {
$settings = $req->getResult()["settings"];
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
$this->createError("Mail is not configured yet.");
return null;
}
$host = $settings["mail_host"] ?? "localhost";
$port = intval($settings["mail_port"] ?? "25");
$login = $settings["mail_username"] ?? "";
$password = $settings["mail_password"] ?? "";
$connectionData = new ConnectionData($host, $port, $login, $password);
$connectionData->setProperty("from", $settings["mail_from"] ?? "");
return $connectionData;
}
return null;
}
public function execute($values = array()): bool { public function execute($values = array()): bool {
if (!parent::execute($values)) { if (!parent::execute($values)) {
return false; return false;
@ -87,12 +93,23 @@ namespace Api\Mail {
return false; return false;
} }
$fromMail = $mailConfig->getProperty('from');
$toMail = $this->getParam('to') ?? $fromMail;
$subject = $this->getParam('subject');
$replyTo = $this->getParam('replyTo');
$replyName = $this->getParam('replyName');
try { try {
$mail = new PHPMailer; $mail = new PHPMailer;
$mail->IsSMTP(); $mail->IsSMTP();
$mail->setFrom($mailConfig->getProperty("from")); $mail->setFrom($fromMail);
$mail->addAddress($this->getParam('to')); $mail->addAddress($toMail);
$mail->Subject = $this->getParam('subject');
if ($replyTo) {
$mail->addReplyTo($replyTo, $replyName);
}
$mail->Subject = $subject;
$mail->SMTPDebug = 0; $mail->SMTPDebug = 0;
$mail->Host = $mailConfig->getHost(); $mail->Host = $mailConfig->getHost();
$mail->Port = $mailConfig->getPort(); $mail->Port = $mailConfig->getPort();
@ -108,6 +125,8 @@ namespace Api\Mail {
if (!$this->success) { if (!$this->success) {
$this->lastError = "Error sending Mail: $mail->ErrorInfo"; $this->lastError = "Error sending Mail: $mail->ErrorInfo";
error_log("sendMail() failed: $mail->ErrorInfo"); error_log("sendMail() failed: $mail->ErrorInfo");
} else {
$this->result["messageId"] = $mail->getLastMessageID();
} }
} catch (Exception $e) { } catch (Exception $e) {
$this->success = false; $this->success = false;
@ -117,4 +136,178 @@ namespace Api\Mail {
return $this->success; return $this->success;
} }
} }
class Sync extends MailAPI {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array());
$this->csrfTokenRequired = true;
}
private function fetchMessageIds() {
$sql = $this->user->getSQL();
$res = $sql->select("uid", "messageId")
->from("ContactRequest")
->where(new Compare("messageId", NULL, "!="))
->execute();
$this->success = ($res !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
}
$messageIds = [];
foreach ($res as $row) {
$messageIds[$row["messageId"]] = $row["uid"];
}
return $messageIds;
}
private function findContactRequest(array &$messageIds, array &$references): ?int {
foreach ($references as &$ref) {
if (isset($messageIds[$ref])) {
return $messageIds[$ref];
}
}
return null;
}
private function parseBody(string $body): string {
// TODO clean this up
return trim($body);
}
private function insertMessages($messages): bool {
$sql = $this->user->getSQL();
$query = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId"])
->onDuplicateKeyStrategy(new UpdateStrategy(["message_id"], ["message" => new Column("message")]));
foreach ($messages as $message) {
$query->addRow(
$message["requestId"],
$sql->select("uid")->from("User")->where(new Compare("email", $message["from"]))->limit(1),
$message["body"],
$message["messageId"]
);
}
$this->success = $query->execute();
$this->lastError = $sql->getLastError();
return $this->success;
}
public function execute($values = array()): bool {
if (!parent::execute($values)) {
return false;
}
$mailConfig = $this->getMailConfig();
if (!$this->success) {
return false;
}
if (!function_exists("imap_open")) {
return $this->createError("IMAP is not enabled. Enable it inside the php config. For more information visit: https://www.php.net/manual/en/imap.setup.php");
}
$messageIds = $this->fetchMessageIds();
if ($messageIds === false) {
return false;
} else if (count($messageIds) === 0) {
// nothing to sync here
return true;
}
// TODO: IMAP mail settings :(
$port = 993;
$folder = ""; // $folder = "INBOX";
$host = str_replace("smtp", "imap", $mailConfig->getHost());
$username = $mailConfig->getLogin();
$password = $mailConfig->getPassword();
$lastSync = intval($mailConfig->getProperty("last_sync", "0"));
$flags = ["/ssl"];
$mailboxStr = '{' . $host . ':' . $port . implode("", $flags) . '}' . $folder;
$mbox = @imap_open($mailboxStr, $username, $password, OP_READONLY);
if (!$mbox) {
return $this->createError("Can't connect to mail server via IMAP: " . imap_last_error());
}
if ($lastSync > 0) {
$lastSyncDateTime = (new \DateTime())->setTimeStamp($lastSync);
$dateStr = $lastSyncDateTime->format("d-M-Y");
$searchCriteria = "SINCE \"$dateStr\"";
} else {
$lastSyncDateTime = null;
$searchCriteria = "ALL";
}
$now = (new \DateTime())->getTimestamp();
$result = @imap_search($mbox, $searchCriteria);
if ($result === false) {
return $this->createError("Could not run search: " . imap_last_error());
}
$messages = [];
foreach ($result as $msgNo) {
$header = imap_headerinfo($mbox, $msgNo);
$date = new \DateTime($header->date);
if ($lastSync === 0 || \datetimeDiff($lastSyncDateTime, $date) > 0) {
$references = property_exists($header, "references") ?
explode(" ", $header->references) : [];
$requestId = $this->findContactRequest($messageIds, $references);
if ($requestId) {
$messageId = $header->message_id;
$senderAddress = null;
if (count($header->from) > 0) {
$senderAddress = $header->from[0]->mailbox . "@" . $header->from[0]->host;
}
// $body = imap_body($mbox, $msgNo);
$structure = imap_fetchstructure($mbox, $msgNo);
$attachments = [];
$hasAttachments = (property_exists($structure, "parts"));
if ($hasAttachments) {
foreach ($structure->parts as $part) {
$disposition = (property_exists($part, "disposition") ? $part->disposition : null);
if ($disposition === "attachment") {
$fileName = array_filter($part->dparameters, function($param) { return $param->attribute === "filename"; });
if (count($fileName) > 0) {
$attachments[] = $fileName[0]->value;
}
}
}
}
$body = imap_fetchbody($mbox, $msgNo, "1");
$body = $this->parseBody($body);
$messages[] = [
"messageId" => $messageId,
"requestId" => $requestId,
"timestamp" => $date->getTimestamp(),
"from" => $senderAddress,
"body" => $body,
"attachments" => $attachments
];
}
}
}
@imap_close($mbox);
if (!$this->insertMessages($messages)) {
return false;
}
$req = new \Api\Settings\Set($this->user);
$this->success = $req->execute(array("settings" => array("mail_last_sync" => "$now")));
$this->lastError = $req->getLastError();
return $this->success;
}
}
} }

@ -151,6 +151,7 @@ class CreateDatabase extends DatabaseScript {
->addRow("mail_username", "", false, false) ->addRow("mail_username", "", false, false)
->addRow("mail_password", "", true, false) ->addRow("mail_password", "", true, false)
->addRow("mail_from", "", false, false) ->addRow("mail_from", "", false, false)
->addRow("mail_last_sync", "", true, false)
->addRow("message_confirm_email", self::MessageConfirmEmail(), false, false) ->addRow("message_confirm_email", self::MessageConfirmEmail(), false, false)
->addRow("message_accept_invite", self::MessageAcceptInvite(), false, false) ->addRow("message_accept_invite", self::MessageAcceptInvite(), false, false)
->addRow("message_reset_password", self::MessageResetPassword(), false, false); ->addRow("message_reset_password", self::MessageResetPassword(), false, false);
@ -163,9 +164,22 @@ class CreateDatabase extends DatabaseScript {
->addString("from_name", 32) ->addString("from_name", 32)
->addString("from_email", 64) ->addString("from_email", 64)
->addString("message", 512) ->addString("message", 512)
->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent)
->addDateTime("created_at", false, $sql->currentTimestamp()) ->addDateTime("created_at", false, $sql->currentTimestamp())
->unique("messageId")
->primaryKey("uid"); ->primaryKey("uid");
$queries[] = $sql->createTable("ContactMessage")
->addSerial("uid")
->addInt("request_id")
->addInt("user_id", true) # null = customer has sent this message
->addString("message", 512)
->addString("messageId", 78)
->addDateTime("created_at", false, $sql->currentTimestamp())
->unique("messageId")
->primaryKey("uid")
->foreignKey("request_id", "ContactRequest", "uid", new CascadeStrategy());
$queries[] = $sql->createTable("ApiPermission") $queries[] = $sql->createTable("ApiPermission")
->addString("method", 32) ->addString("method", 32)
->addJson("groups", true, '[]') ->addJson("groups", true, '[]')
@ -183,6 +197,7 @@ class CreateDatabase extends DatabaseScript {
->addRow("Routes/fetch", array(USER_GROUP_ADMIN), "Allows users to list all configured routes") ->addRow("Routes/fetch", array(USER_GROUP_ADMIN), "Allows users to list all configured routes")
->addRow("Routes/save", array(USER_GROUP_ADMIN), "Allows users to create, delete and modify routes") ->addRow("Routes/save", array(USER_GROUP_ADMIN), "Allows users to create, delete and modify routes")
->addRow("Mail/test", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to send a test email to a given address") ->addRow("Mail/test", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to send a test email to a given address")
->addRow("Mail/Sync", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to synchronize mails with the database")
->addRow("Settings/get", array(USER_GROUP_ADMIN), "Allows users to fetch server settings") ->addRow("Settings/get", array(USER_GROUP_ADMIN), "Allows users to fetch server settings")
->addRow("Settings/set", array(USER_GROUP_ADMIN), "Allows users create, delete or modify server settings") ->addRow("Settings/set", array(USER_GROUP_ADMIN), "Allows users create, delete or modify server settings")
->addRow("Stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch server stats") ->addRow("Stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch server stats")

@ -110,4 +110,8 @@ class Settings {
public function isRegistrationAllowed(): bool { public function isRegistrationAllowed(): bool {
return $this->registrationAllowed; return $this->registrationAllowed;
} }
public function isMailEnabled(): bool {
return $this->mailEnabled;
}
} }

@ -59,7 +59,7 @@ class AlterTable extends Query {
public function getConstraint(): ?Constraint { return $this->constraint; } public function getConstraint(): ?Constraint { return $this->constraint; }
public function getTable(): string { return $this->table; } public function getTable(): string { return $this->table; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$tableName = $this->sql->tableName($this->getTable()); $tableName = $this->sql->tableName($this->getTable());
$action = $this->getAction(); $action = $this->getAction();
$column = $this->getColumn(); $column = $this->getColumn();

@ -36,7 +36,7 @@ class CreateProcedure extends Query {
return $this; return $this;
} }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$head = $this->sql->getProcedureHead($this); $head = $this->sql->getProcedureHead($this);
$body = $this->sql->getProcedureBody($this); $body = $this->sql->getProcedureBody($this);
$tail = $this->sql->getProcedureTail(); $tail = $this->sql->getProcedureTail();

@ -61,7 +61,7 @@ class CreateTrigger extends Query {
public function getTable(): string { return $this->tableName; } public function getTable(): string { return $this->tableName; }
public function getProcedure(): CreateProcedure { return $this->procedure; } public function getProcedure(): CreateProcedure { return $this->procedure; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$name = $this->sql->tableName($this->getName()); $name = $this->sql->tableName($this->getName());
$time = $this->getTime(); $time = $this->getTime();
$event = $this->getEvent(); $event = $this->getEvent();

@ -24,7 +24,7 @@ class Delete extends Query {
public function getTable(): string { return $this->table; } public function getTable(): string { return $this->table; }
public function getConditions(): array { return $this->conditions; } public function getConditions(): array { return $this->conditions; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$table = $this->sql->tableName($this->getTable()); $table = $this->sql->tableName($this->getTable());
$where = $this->sql->getWhereClause($this->getConditions(), $params); $where = $this->sql->getWhereClause($this->getConditions(), $params);
return "DELETE FROM $table$where"; return "DELETE FROM $table$where";

@ -23,7 +23,7 @@ class Drop extends Query {
return $this->table; return $this->table;
} }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
return "DROP TABLE " . $this->sql->tableName($this->getTable()); return "DROP TABLE " . $this->sql->tableName($this->getTable());
} }
} }

@ -43,7 +43,7 @@ class Insert extends Query {
public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; } public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; }
public function getReturning(): ?string { return $this->returning; } public function getReturning(): ?string { return $this->returning; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$tableName = $this->sql->tableName($this->getTableName()); $tableName = $this->sql->tableName($this->getTableName());
$columns = $this->getColumns(); $columns = $this->getColumns();
$rows = $this->getRows(); $rows = $this->getRows();

@ -24,4 +24,6 @@ abstract class Query extends Expression {
public function execute() { public function execute() {
return $this->sql->executeQuery($this); return $this->sql->executeQuery($this);
} }
public abstract function build(array &$params): ?string;
} }

@ -95,7 +95,7 @@ class Select extends Query {
public function getOffset(): int { return $this->offset; } public function getOffset(): int { return $this->offset; }
public function getGroupBy(): array { return $this->groupColumns; } public function getGroupBy(): array { return $this->groupColumns; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$columns = $this->sql->columnName($this->getColumns()); $columns = $this->sql->columnName($this->getColumns());
$tables = $this->getTables(); $tables = $this->getTables();

@ -15,7 +15,7 @@ class Truncate extends Query {
public function getTable(): string { return $this->tableName; } public function getTable(): string { return $this->tableName; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
return "TRUNCATE " . $this->sql->tableName($this->getTable()); return "TRUNCATE " . $this->sql->tableName($this->getTable());
} }
} }

@ -32,7 +32,7 @@ class Update extends Query {
public function getConditions(): array { return $this->conditions; } public function getConditions(): array { return $this->conditions; }
public function getValues(): array { return $this->values; } public function getValues(): array { return $this->values; }
public function build(array &$params, Query $context = NULL): ?string { public function build(array &$params): ?string {
$table = $this->sql->tableName($this->getTable()); $table = $this->sql->tableName($this->getTable());
$valueStr = array(); $valueStr = array();

@ -315,6 +315,8 @@ abstract class SQL {
protected function createExpression(Expression $exp, array &$params) { protected function createExpression(Expression $exp, array &$params) {
if ($exp instanceof Column) { if ($exp instanceof Column) {
return $this->columnName($exp); return $this->columnName($exp);
} else if ($exp instanceof Query) {
return "(" . $exp->build($params) . ")";
} else { } else {
$this->lastError = "Unsupported expression type: " . get_class($exp); $this->lastError = "Unsupported expression type: " . get_class($exp);
return null; return null;

@ -1,6 +1,6 @@
<?php <?php
define("WEBBASE_VERSION", "1.2.4"); define("WEBBASE_VERSION", "1.2.5");
spl_autoload_extensions(".php"); spl_autoload_extensions(".php");
spl_autoload_register(function($class) { spl_autoload_register(function($class) {