Browse Source

Contact Mails

Roman 3 years ago
parent
commit
779550cab4

+ 24 - 0
cli.php

@@ -1,6 +1,7 @@
 <?php
 
 include_once 'core/core.php';
+require_once 'core/datetime.php';
 include_once 'core/constants.php';
 
 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'];
 if (count($argv) < 2) {
   _exit("Usage: cli.php <db|routes|settings|maintenance> [options...]");
@@ -481,6 +502,9 @@ switch ($command) {
   case 'test':
     onTest($argv);
     break;
+  case 'mail':
+    onMail($argv);
+    break;
   default:
     printLine("Unknown command '$command'");
     printLine();

+ 41 - 16
core/Api/ContactAPI.class.php

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

+ 225 - 32
core/Api/MailAPI.class.php

@@ -1,8 +1,35 @@
 <?php
 
 namespace Api {
+
+  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\Parameter\Parameter;
   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\PHPMailer;
-  use Objects\ConnectionData;
   use Objects\User;
 
   class Test extends MailAPI {
@@ -45,40 +74,17 @@ namespace Api\Mail {
   class Send extends MailAPI {
     public function __construct($user, $externalCall = false) {
       parent::__construct($user, $externalCall, array(
-        'to' => new Parameter('to', Parameter::TYPE_EMAIL),
-        'subject'  => new StringType('subject', -1),
+        'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null),
+        'subject' => new StringType('subject', -1),
         'body' => new StringType('body', -1),
+        'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
+        'replyName' => new StringType('replyName', 32, true, "")
       ));
       $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 {
-      if(!parent::execute($values)) {
+      if (!parent::execute($values)) {
         return false;
       }
 
@@ -87,12 +93,23 @@ namespace Api\Mail {
         return false;
       }
 
+      $fromMail = $mailConfig->getProperty('from');
+      $toMail = $this->getParam('to') ?? $fromMail;
+      $subject = $this->getParam('subject');
+      $replyTo = $this->getParam('replyTo');
+      $replyName = $this->getParam('replyName');
+
       try {
         $mail = new PHPMailer;
         $mail->IsSMTP();
-        $mail->setFrom($mailConfig->getProperty("from"));
-        $mail->addAddress($this->getParam('to'));
-        $mail->Subject = $this->getParam('subject');
+        $mail->setFrom($fromMail);
+        $mail->addAddress($toMail);
+
+        if ($replyTo) {
+          $mail->addReplyTo($replyTo, $replyName);
+        }
+
+        $mail->Subject = $subject;
         $mail->SMTPDebug = 0;
         $mail->Host = $mailConfig->getHost();
         $mail->Port = $mailConfig->getPort();
@@ -108,6 +125,8 @@ namespace Api\Mail {
         if (!$this->success) {
           $this->lastError = "Error sending Mail: $mail->ErrorInfo";
           error_log("sendMail() failed: $mail->ErrorInfo");
+        } else {
+          $this->result["messageId"] = $mail->getLastMessageID();
         }
       } catch (Exception $e) {
         $this->success = false;
@@ -117,4 +136,178 @@ namespace Api\Mail {
       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;
+    }
+  }
 }

+ 15 - 0
core/Configuration/CreateDatabase.class.php

@@ -151,6 +151,7 @@ class CreateDatabase extends DatabaseScript {
       ->addRow("mail_username", "", false, false)
       ->addRow("mail_password", "", true, false)
       ->addRow("mail_from", "", false, false)
+      ->addRow("mail_last_sync", "", true, false)
       ->addRow("message_confirm_email", self::MessageConfirmEmail(), false, false)
       ->addRow("message_accept_invite", self::MessageAcceptInvite(), false, false)
       ->addRow("message_reset_password", self::MessageResetPassword(), false, false);
@@ -163,9 +164,22 @@ class CreateDatabase extends DatabaseScript {
       ->addString("from_name", 32)
       ->addString("from_email", 64)
       ->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())
+      ->unique("messageId")
       ->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")
       ->addString("method", 32)
       ->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/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/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/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")

+ 4 - 0
core/Configuration/Settings.class.php

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

+ 1 - 1
core/Driver/SQL/Query/AlterTable.class.php

@@ -59,7 +59,7 @@ class AlterTable extends Query {
   public function getConstraint(): ?Constraint { return $this->constraint; }
   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());
     $action = $this->getAction();
     $column = $this->getColumn();

+ 1 - 1
core/Driver/SQL/Query/CreateProcedure.class.php

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

+ 1 - 1
core/Driver/SQL/Query/CreateTrigger.class.php

@@ -61,7 +61,7 @@ class CreateTrigger extends Query {
   public function getTable(): string { return $this->tableName; }
   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());
     $time = $this->getTime();
     $event = $this->getEvent();

+ 1 - 1
core/Driver/SQL/Query/Delete.class.php

@@ -24,7 +24,7 @@ class Delete extends Query {
   public function getTable(): string { return $this->table; }
   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());
     $where = $this->sql->getWhereClause($this->getConditions(), $params);
     return "DELETE FROM $table$where";

+ 1 - 1
core/Driver/SQL/Query/Drop.php

@@ -23,7 +23,7 @@ class Drop extends Query {
     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());
   }
 }

+ 1 - 1
core/Driver/SQL/Query/Insert.class.php

@@ -43,7 +43,7 @@ class Insert extends Query {
   public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; }
   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());
     $columns = $this->getColumns();
     $rows = $this->getRows();

+ 2 - 0
core/Driver/SQL/Query/Query.class.php

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

+ 1 - 1
core/Driver/SQL/Query/Select.class.php

@@ -95,7 +95,7 @@ class Select extends Query {
   public function getOffset(): int { return $this->offset; }
   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());
     $tables = $this->getTables();
 

+ 1 - 1
core/Driver/SQL/Query/Truncate.class.php

@@ -15,7 +15,7 @@ class Truncate extends Query {
 
   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());
   }
 }

+ 1 - 1
core/Driver/SQL/Query/Update.class.php

@@ -32,7 +32,7 @@ class Update extends Query {
   public function getConditions(): array { return $this->conditions; }
   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());
 
     $valueStr = array();

+ 2 - 0
core/Driver/SQL/SQL.class.php

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

+ 1 - 1
core/core.php

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