Browse Source

Add imapsync tool

andryyy 8 years ago
parent
commit
96ae33ee6a

+ 77 - 1
data/web/add.php

@@ -1,6 +1,6 @@
 <?php
 require_once("inc/prerequisites.inc.php");
-$AuthUsers = array("admin", "domainadmin");
+$AuthUsers = array("admin", "domainadmin", "user");
 if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
 	header('Location: /');
 	exit();
@@ -259,6 +259,82 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 	<?php
 	}
 }
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+  if (isset($_GET['syncjob'])) {
+?>
+				<h4><?=$lang['add']['syncjob'];?></h4>
+				<p><?=$lang['add']['syncjob_hint'];?></p>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="host1"><?=$lang['add']['hostname'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="host1" id="host1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="port1">Port</label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="user1"><?=$lang['add']['username'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="user1" id="user1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password1"><?=$lang['add']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="password1" id="password1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
+						<div class="col-sm-10">
+							<select name="enc1" id="enc1" title="<?=$lang['add']['select'];?>" required>
+                <option selected>TLS</option>
+                <option>SSL</option>
+                <option>PLAIN</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['add']['mins_interval'];?></label>
+						<div class="col-sm-10">
+              <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="20" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="exclude"><?=$lang['add']['exclude'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="exclude" id="exclude" value="(?i)spam|(?i)junk">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="trigger_add_syncjob" value="1" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+}
+
 else {
 ?>
 				<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>

+ 45 - 1
data/web/delete.php

@@ -1,6 +1,6 @@
 <?php
 require_once("inc/prerequisites.inc.php");
-$AuthUsers = array("admin", "domainadmin");
+$AuthUsers = array("admin", "domainadmin", "user");
 if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
 	header('Location: /');
 	exit();
@@ -148,6 +148,50 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 		<?php
 		}
 }
+elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+		// DELETE SYNCJOB
+		if (isset($_GET["syncjob"]) &&
+			is_numeric($_GET["syncjob"]) &&
+      filter_var($_SESSION['mailcow_cc_username'], FILTER_VALIDATE_EMAIL)) {
+        try {
+          $stmt = $pdo->prepare("SELECT `user2` FROM `imapsync`
+              WHERE `id` = :id AND user2 = :user2");
+          $stmt->execute(array(':id' => $_GET["syncjob"], ':user2' => $_SESSION['mailcow_cc_username']));
+          $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+        }
+        catch(PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+        }
+				if ($num_results != 0 && !empty($num_results)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_syncjob_warning'], htmlspecialchars($_SESSION['mailcow_cc_username']));?></div>
+					<p><?=$lang['delete']['remove_syncjob_details'];?></p>
+					<form class="form-horizontal" role="form" method="post" action="/user.php">
+					<input type="hidden" name="username" value="<?=htmlspecialchars($mailbox);?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<input type="hidden" name="id" value="<?=$_GET["syncjob"];?>">
+								<button type="submit" name="trigger_delete_syncjob" value="1" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		else {
+		?>
+			<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+		<?php
+		}
+}
 else {
 ?>
 	<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>

+ 99 - 1
data/web/edit.php

@@ -1,6 +1,6 @@
 <?php
 require_once("inc/prerequisites.inc.php");
-$AuthUsers = array("admin", "domainadmin");
+$AuthUsers = array("admin", "domainadmin", "user");
 if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
 	header('Location: /');
 	exit();
@@ -517,6 +517,104 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 	<?php
 	}
 }
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+	if (isset($_GET['syncjob']) &&
+    is_numeric($_GET["syncjob"]) &&
+    filter_var($_SESSION['mailcow_cc_username'], FILTER_VALIDATE_EMAIL)) {
+			$id = $_GET["syncjob"];
+      $username = $_SESSION['mailcow_cc_username'];
+			try {
+				$stmt = $pdo->prepare("SELECT * FROM `imapsync` WHERE `user2` = :username AND id = :id");
+				$stmt->execute(array(
+					':username' => $username,
+					':id' => $id
+				));
+				$result = $stmt->fetch(PDO::FETCH_ASSOC);
+			}
+			catch(PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+      }
+      if ($result && !empty($result)) {
+			?>
+				<h4><?=$lang['edit']['syncjob'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+				<input type="hidden" name="id" value="<?=htmlspecialchars($result['id']);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="host1"><?=$lang['edit']['hostname'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="host1" id="host1" value="<?=htmlspecialchars($result['host1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="port1">Port</label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="<?=htmlspecialchars($result['port1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="user1"><?=$lang['edit']['username'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="user1" id="user1" value="<?=htmlspecialchars($result['user1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password1"><?=$lang['edit']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="password1" id="password1" value="<?=htmlspecialchars($result['password1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?>:</label>
+						<div class="col-sm-10">
+							<select id="enc1" name="enc1">
+								<option <?=($result['enc1'] == "TLS") ? "selected" : null;?>>TLS</option>
+								<option <?=($result['enc1'] == "SSL") ? "selected" : null;?>>SSL</option>
+								<option <?=($result['enc1'] == "PLAIN") ? "selected" : null;?>>PLAIN</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['edit']['mins_interval'];?></label>
+						<div class="col-sm-10">
+              <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="<?=htmlspecialchars($result['mins_interval'], ENT_QUOTES, 'UTF-8');?>" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="exclude"><?=$lang['edit']['exclude'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="exclude" id="exclude" value="<?=htmlspecialchars($result['exclude'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" <?=($result['active']=="1") ? "checked" : "";?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="trigger_edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+			<?php
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	else {
+	?>
+		<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+	<?php
+	}
+}
 else {
 ?>
 	<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>

+ 265 - 0
data/web/inc/functions.inc.php

@@ -84,6 +84,30 @@ function init_db_schema() {
   if ($num_results == 0) {
     $pdo->query("ALTER TABLE `mailbox` ADD `wants_tagged_subject` tinyint(1) NOT NULL DEFAULT '0'");
   }
+  $stmt = $pdo->query("SELECT * FROM information_schema.TABLES WHERE TABLE_NAME = 'imapsync'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+
+  if ($num_results == 0) {
+    $pdo->query("CREATE TABLE IF NOT EXISTS `imapsync` (
+      `id` int NOT NULL AUTO_INCREMENT,
+      `user2` VARCHAR(255) NOT NULL,
+      `host1` VARCHAR(255) NOT NULL,
+      `authmech1` ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN',
+      `user1` VARCHAR(255) NOT NULL,
+      `exclude` VARCHAR(500) NOT NULL DEFAULT '',
+      `password1` VARCHAR(255) NOT NULL,
+      `mins_interval` VARCHAR(50) NOT NULL,
+      `port1` SMALLINT NOT NULL,
+      `enc1` ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS',
+      `delete2duplicates` TINYINT(1) NOT NULL DEFAULT '1',
+      `returned_text` TEXT,
+      `last_run` TIMESTAMP NULL DEFAULT NULL,
+      `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      `active` TINYINT(1) NOT NULL DEFAULT '0',
+      PRIMARY KEY (`id`)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;");
+  }
 }
 function verify_ssha256($hash, $password) {
 	// Remove tag if any
@@ -2466,6 +2490,247 @@ function set_tls_policy($postarray) {
 		'msg' => sprintf($lang['success']['mailbox_modified'], $username)
 	);
 }
+function set_syncjob($postarray, $action) {
+	global $lang;
+	global $pdo;
+  $username = $_SESSION['mailcow_cc_username'];
+  if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if ($_SESSION['mailcow_cc_role'] != "user") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  // DELETE
+  if ($action == "delete") {
+    $id = $postarray['id'];
+    if (!is_numeric($id)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username AND `id`= :id");
+      $stmt->execute(array(
+        ':username' => $username,
+        ':id' => $id,
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+    );
+    return true;
+  }
+  elseif ($action == "add") {
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+    $port1            = $postarray['port1'];
+    $host1            = $postarray['host1'];
+    $password1        = $postarray['password1'];
+    $exclude          = $postarray['exclude'];
+    $user1            = $postarray['user1'];
+    $mins_interval    = $postarray['mins_interval'];
+    $enc1             = $postarray['enc1'];
+
+    if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 10, 'max_range' => 3600)))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (!is_valid_domain_name($host1)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (@preg_match("/" . $exclude . "/", null) === false) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("SELECT `user2`, `user1` FROM `imapsync`
+        WHERE `user2` = :user2 AND `user1` = :user1");
+      $stmt->execute(array(':user1' => $user1, ':user2' => $username));
+      $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($host1 . ' / ' . $user1))
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
+        VALUES (:user2, :exclude, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
+      $stmt->execute(array(
+        ':user2' => $username,
+        ':exclude' => $exclude,
+        ':host1' => $host1,
+        ':authmech1' => 'PLAIN',
+        ':user1' => $user1,
+        ':password1' => $password1,
+        ':mins_interval' => $mins_interval,
+        ':port1' => $port1,
+        ':enc1' => $enc1,
+        ':delete2duplicates' => $delete2duplicates,
+        ':active' => $active,
+      ));
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+    );
+    return true;
+  }
+  elseif ($action == "edit") {
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+    $id               = $postarray['id'];
+    $port1            = $postarray['port1'];
+    $host1            = $postarray['host1'];
+    $password1        = $postarray['password1'];
+    $exclude          = $postarray['exclude'];
+    $user1            = $postarray['user1'];
+    $mins_interval    = $postarray['mins_interval'];
+    $enc1             = $postarray['enc1'];
+    if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 10, 'max_range' => 3600)))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (!is_valid_domain_name($host1)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    if (@preg_match("/" . $exclude . "/", null) === false) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("SELECT `user2` FROM `imapsync`
+        WHERE `user2` = :user2 AND `id` = :id");
+      $stmt->execute(array(':user2' => $username, ':id' => $id));
+      $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+    if (empty($num_results)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("UPDATE `imapsync` set `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
+        WHERE `user2` = :user2 AND `id` = :id");
+      $stmt->execute(array(
+        ':user2' => $username,
+        ':id' => $id,
+        ':exclude' => $exclude,
+        ':host1' => $host1,
+        ':user1' => $user1,
+        ':password1' => $password1,
+        ':mins_interval' => $mins_interval,
+        ':port1' => $port1,
+        ':enc1' => $enc1,
+        ':delete2duplicates' => $delete2duplicates,
+        ':active' => $active,
+      ));
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+    );
+    return true;
+  }
+}
 function get_tls_policy($username) {
 	global $lang;
 	global $pdo;

+ 20 - 0
data/web/inc/init.sql

@@ -102,6 +102,26 @@ CREATE TABLE IF NOT EXISTS `filterconf` (
 	KEY `object` (`object`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
 
+CREATE TABLE IF NOT EXISTS `imapsync` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `user2` varchar(255) NOT NULL,
+  `host1` varchar(255) NOT NULL,
+  `authmech1` ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN',
+  `user1` varchar(255) NOT NULL,
+  `password1` varchar(255) NOT NULL,
+  `exclude` VARCHAR(500) NOT NULL DEFAULT '',
+  `mins_interval` VARCHAR(50) NOT NULL,
+  `port1` SMALLINT NOT NULL,
+  `enc1` ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS',
+  `delete2duplicates` TINYINT(1) NOT NULL DEFAULT '1',
+  `returned_text` TEXT,
+  `last_run` TIMESTAMP NULL DEFAULT NULL,
+  `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `active` TINYINT(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
 DROP VIEW IF EXISTS grouped_mail_aliases;
 DROP VIEW IF EXISTS grouped_sender_acl;
 DROP VIEW IF EXISTS grouped_domain_alias_address;

+ 9 - 0
data/web/inc/triggers.inc.php

@@ -60,6 +60,15 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user
 	if (isset($_POST["trigger_set_tls_policy"])) {
 		set_tls_policy($_POST);
 	}
+	if (isset($_POST["trigger_add_syncjob"])) {
+		set_syncjob($_POST, "add");
+	}
+	if (isset($_POST["trigger_edit_syncjob"])) {
+		set_syncjob($_POST, "edit");
+	}
+	if (isset($_POST["trigger_delete_syncjob"])) {
+		set_syncjob($_POST, "delete");
+	}
 	if (isset($_POST["trigger_set_time_limited_aliases"])) {
 		set_time_limited_aliases($_POST);
 	}

+ 1 - 1
data/web/inc/vars.inc.php

@@ -1,5 +1,5 @@
 <?php
-error_reporting(0);
+error_reporting(E_ALL);
 
 /*
 PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!

+ 6 - 0
data/web/js/user.js

@@ -25,4 +25,10 @@ $(document).ready(function() {
 	$.fn.bootstrapSwitch.defaults.onColor = 'success';
 	$("[name='tls_out']").bootstrapSwitch();
 	$("[name='tls_in']").bootstrapSwitch();
+
+  // Log modal
+  $('#logModal').on('show.bs.modal', function(e) {
+  var logText = $(e.relatedTarget).data('log-text');
+  $(e.currentTarget).find('#logText').html('<pre>' + logText + '</pre>');
+  });
 });

+ 31 - 0
data/web/lang/lang.de.php

@@ -111,7 +111,9 @@ $lang['user']['alias_valid_until'] = 'Gültig bis';
 $lang['user']['alias_remove_all'] = 'Alle entfernen';
 $lang['user']['alias_time_left'] = 'Zeit verbleibend';
 $lang['user']['alias_full_date'] = 'd.m.Y, H:i:s T';
+$lang['user']['syncjob_full_date'] = 'd.m.Y, H:i:s T';
 $lang['user']['alias_select_validity'] = 'Bitte Gültigkeit auswählen';
+$lang['user']['sync_jobs'] = 'Sync Jobs';
 $lang['user']['hour'] = 'Stunde';
 $lang['user']['hours'] = 'Stunden';
 $lang['user']['day'] = 'Tag';
@@ -151,6 +153,18 @@ $lang['user']['tag_help_explain'] = 'Als Unterordner: Es wird ein Ordner mit dem
 In Betreff: Der Name des Tags wird dem Betreff angefügt, etwa "[Facebook] Meine Neuigkeiten".';
 $lang['user']['tag_help_example'] = 'Beispiel für eine getaggte E-Mail-Adresse: ich<b>+Facebook</b>@example.org';
 
+$lang['user']['encryption'] = 'Verschlüsselung';
+$lang['user']['username'] = 'Benutzername';
+$lang['user']['password'] = 'Password';
+$lang['user']['last_run'] = 'Letzte Ausführung';
+$lang['user']['excludes'] = 'Ausschlüsse';
+$lang['user']['interval'] = 'Intervall';
+$lang['user']['active'] = 'Aktiv';
+$lang['user']['action'] = 'Aktion';
+$lang['user']['edit'] = 'Bearbeiten';
+$lang['user']['remove'] = 'Entfernen';
+$lang['user']['create_syncjob'] = 'Neuen Sync-Job erstellen';
+
 $lang['start']['dashboard'] = '%s - Dashboard';
 $lang['start']['start_rc'] = 'Roundcube öffnen';
 $lang['start']['start_sogo'] = 'SOGo öffnen';
@@ -219,14 +233,22 @@ $lang['delete']['remove_domain_warning'] = '<b>Warnung:</b> Sie entfernen die Do
 $lang['delete']['remove_domainalias_warning'] = '<b>Warnung:</b> Sie entfernen die Alias-Domain <b>%s</b>!';
 $lang['delete']['remove_domainadmin_warning'] = '<b>Warnung:</b> Sie entfernen den Domain-Administrator <b>%s</b>!';
 $lang['delete']['remove_alias_warning'] = '<b>Warnung:</b> Sie entfernen die Alias-Adresse <b>%s</b>!';
+$lang['delete']['remove_syncjob_warning'] = '<b>Warnung:</b> Sie entfernen einen Sync-Job des Benutzers <b>%s</b>!';
 $lang['delete']['remove_mailbox_warning'] = '<b>Warnung:</b> Sie entfernen die Mailbox <b>%s</b>!';
 $lang['delete']['remove_mailbox_details'] = 'Die Mailbox wird <b>vollständig und permanent</b> entfernt!';
 $lang['delete']['remove_domain_details'] = 'Diese Aktion entfernt ebenfalls Domain-Aliasse.<br /><br /><b>Eine Domain muss leer sein, um entfernt zu werden.</b>';
+$lang['delete']['remove_syncjob_details'] = 'Objekte dieses Sync-Jobs werden nicht mehr vom entfernten Server abgeholt.';
 $lang['delete']['remove_alias_details'] = 'Benutzer werden keine Nachrichten mehr von dieser Adresse erhalten und versenden koennen!</b>';
 $lang['delete']['remove_button'] = 'Entfernen';
 $lang['delete']['previous'] = 'Vorherige Seite';
 
+$lang['edit']['syncjob'] = 'Sync-Job bearbeiten';
 $lang['edit']['save'] = 'Änderungen speichern';
+$lang['edit']['username'] = 'Benutzername';
+$lang['edit']['hostname'] = 'Servername';
+$lang['edit']['encryption'] = 'Verschlüsselungsmethode';
+$lang['edit']['mins_interval'] = 'Intervall (min)';
+$lang['edit']['exclude'] = 'Elemente ausschließen (Regex)';
 $lang['edit']['archive'] = 'Archiv-Zugriff';
 $lang['edit']['max_mailboxes'] = 'Max. Mailboxanzahl:';
 $lang['edit']['title'] = 'Objekt bearbeiten';
@@ -265,6 +287,15 @@ $lang['edit']['previous'] = 'Vorherige Seite';
 $lang['edit']['unchanged_if_empty'] = 'Unverändert, wenn leer';
 $lang['edit']['dont_check_sender_acl'] = 'Absender für Domain %s nicht prüfen';
 
+$lang['add']['syncjob'] = 'Sync-Job erstellen';
+$lang['add']['syncjob_hint'] = 'Passwörter werden unverschlüsselt abgelegt!';
+$lang['add']['hostname'] = 'Servername';
+$lang['add']['username'] = 'Benutzername';
+$lang['add']['enc_method'] = 'Verschlüsselungsmethode';
+$lang['add']['mins_interval'] = 'Abrufintervall (Minuten)';
+$lang['add']['exclude'] = 'Elemente ausschließen (Regex)';
+$lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel';
+
 $lang['add']['title'] = 'Objekt anlegen';
 $lang['add']['domain'] = 'Domain';
 $lang['add']['active'] = 'Aktiv';

+ 45 - 13
data/web/lang/lang.en.php

@@ -106,14 +106,16 @@ $lang['user']['alias'] = 'Alias';
 $lang['user']['aliases'] = 'Aliases';
 $lang['user']['is_catch_all'] = 'Catch-all for domain/s';
 $lang['user']['aliases_also_send_as'] = 'Also allowed to send as';
-$lang['user']['aliases_send_as_all'] = 'Do not check sender access for following domains';
+$lang['user']['aliases_send_as_all'] = 'Do not check sender access for following domain/s';
 $lang['user']['alias_create_random'] = 'Generate random alias';
 $lang['user']['alias_extend_all'] = 'Extend aliases by 1 hour';
 $lang['user']['alias_valid_until'] = 'Valid until';
 $lang['user']['alias_remove_all'] = 'Remove all aliases';
 $lang['user']['alias_time_left'] = 'Time left';
 $lang['user']['alias_full_date'] = 'd.m.Y, H:i:s T';
+$lang['user']['syncjob_full_date'] = 'd.m.Y, H:i:s T';
 $lang['user']['alias_select_validity'] = 'Period of validity';
+$lang['user']['sync_jobs'] = 'Sync jobs';
 $lang['user']['hour'] = 'Hour';
 $lang['user']['hours'] = 'Hours';
 $lang['user']['day'] = 'Day';
@@ -153,6 +155,18 @@ $lang['user']['tag_help_explain'] = 'In subfolder: a new subfolder named after t
 In subject: the tags name will be prepended to the mails subject, example: "[Facebook] Meine Neuigkeiten".';
 $lang['user']['tag_help_example'] = 'Example for a tagged email address: ich<b>+Facebook</b>@example.org';
 
+$lang['user']['encryption'] = 'Encyrption';
+$lang['user']['username'] = 'Username';
+$lang['user']['password'] = 'Password';
+$lang['user']['last_run'] = 'Last run';
+$lang['user']['excludes'] = 'Excludes';
+$lang['user']['interval'] = 'Interval';
+$lang['user']['active'] = 'Active';
+$lang['user']['action'] = 'Action';
+$lang['user']['edit'] = 'Edit';
+$lang['user']['remove'] = 'Remove';
+$lang['user']['create_syncjob'] = 'Create new sync job';
+
 $lang['start']['dashboard'] = '%s - dashboard';
 $lang['start']['start_rc'] = 'Open Roundcube';
 $lang['start']['start_sogo'] = 'Open SOGo';
@@ -218,27 +232,36 @@ $lang['info']['no_action'] = 'No action applicable';
 
 $lang['delete']['title'] = 'Remove object';
 $lang['delete']['remove_domain_warning'] = '<b>Warning:</b> You are about to remove the domain <b>%s</b>!';
+$lang['delete']['remove_syncjob_warning'] = '<b>Warning:</b> You are about to remove a sync job for user <b>%s</b>!';
 $lang['delete']['remove_domainalias_warning'] = '<b>Warning:</b> You are about to remove the domain alias <b>%s</b>!';
 $lang['delete']['remove_domainadmin_warning'] = '<b>Warning:</b> You are about to remove the domain administrator <b>%s</b>!';
 $lang['delete']['remove_alias_warning'] = '<b>Warning:</b> You are about to remove the alias address <b>%s</b>!';
 $lang['delete']['remove_mailbox_warning'] = '<b>Warning:</b> You are about to remove the mailbox <b>%s</b>!';
 $lang['delete']['remove_mailbox_details'] = 'The mailbox will be <b>purged permanently</b>!';
 $lang['delete']['remove_domain_details'] = 'This also removes domain aliases.<br /><br /><b>A domain must be empty to be removed.</b>';
+$lang['delete']['remove_syncjob_details'] = 'Objects from this sync job will not be pulled from the remote server anymore.';
 $lang['delete']['remove_alias_details'] = 'Users will no longer be able to receive mail for or send mail from this address.</b>';
 $lang['delete']['remove_button'] = 'Remove';
 $lang['delete']['previous'] = 'Previous page';
 
+$lang['edit']['syncjob'] = 'Edit sync job';
+$lang['edit']['save'] = 'Save changes';
+$lang['edit']['username'] = 'Save changes';
+$lang['edit']['hostname'] = 'Hostname';
+$lang['edit']['encryption'] = 'Encryption';
+$lang['edit']['mins_interval'] = 'Interval (min)';
+$lang['edit']['exclude'] = 'Exclude objects (regex)';
 $lang['edit']['save'] = 'Save changes';
 $lang['edit']['archive'] = 'Archive access';
-$lang['edit']['max_mailboxes'] = 'Max. possible mailboxes:';
+$lang['edit']['max_mailboxes'] = 'Max. possible mailboxes';
 $lang['edit']['title'] = 'Edit object';
-$lang['edit']['target_address'] = 'Goto address/es <small>(comma-separated)</small>:';
+$lang['edit']['target_address'] = 'Goto address/es <small>(comma-separated)</small>';
 $lang['edit']['active'] = 'Active';
-$lang['edit']['target_domain'] = 'Target domain:';
-$lang['edit']['password'] = 'Password:';
-$lang['edit']['ratelimit'] = 'Outgoing rate limit/h:';
+$lang['edit']['target_domain'] = 'Target domain';
+$lang['edit']['password'] = 'Password';
+$lang['edit']['ratelimit'] = 'Outgoing rate limit/h';
 $lang['danger']['ratelimt_less_one'] = 'Outgoing rate limit/h must not be less than 1';
-$lang['edit']['password_repeat'] = 'Confirmation password (repeat):';
+$lang['edit']['password_repeat'] = 'Confirmation password (repeat)';
 $lang['edit']['domain_admin'] = 'Edit domain administrator';
 $lang['edit']['domain'] = 'Edit domain';
 $lang['edit']['alias_domain'] = 'Alias domain';
@@ -247,14 +270,14 @@ $lang['edit']['domains'] = 'Domains';
 $lang['edit']['destroy'] = 'Manual data input';
 $lang['edit']['alias'] = 'Edit alias';
 $lang['edit']['mailbox'] = 'Edit mailbox';
-$lang['edit']['description'] = 'Description:';
-$lang['edit']['max_aliases'] = 'Max. aliases:';
-$lang['edit']['max_quota'] = 'Max. quota per mailbox (MiB):';
-$lang['edit']['domain_quota'] = 'Domain quota:';
-$lang['edit']['backup_mx_options'] = 'Backup MX options:';
+$lang['edit']['description'] = 'Description';
+$lang['edit']['max_aliases'] = 'Max. aliases';
+$lang['edit']['max_quota'] = 'Max. quota per mailbox (MiB)';
+$lang['edit']['domain_quota'] = 'Domain quota';
+$lang['edit']['backup_mx_options'] = 'Backup MX options';
 $lang['edit']['relay_domain'] = 'Relay domain';
 $lang['edit']['relay_all'] = 'Relay all recipients';
-$lang['edit']['dkim_signature'] = 'DKIM signature:';
+$lang['edit']['dkim_signature'] = 'DKIM signature';
 $lang['edit']['dkim_record_info'] = '<small>Please add a TXT record with the given value to your DNS settings.</small>';
 $lang['edit']['relay_all_info'] = '<small>If you choose <b>not</b> to relay all recipients, you will need to add a ("blind") mailbox for every single recipient that should be relayed.</small>';
 $lang['edit']['full_name'] = 'Full name';
@@ -267,6 +290,15 @@ $lang['edit']['previous'] = 'Previous page';
 $lang['edit']['unchanged_if_empty'] = 'If unchanged leave blank';
 $lang['edit']['dont_check_sender_acl'] = 'Do not check sender for domain %s';
 
+$lang['add']['syncjob'] = 'Add sync job';
+$lang['add']['syncjob_hint'] = 'Be aware that passwords need to be saved plain-text!';
+$lang['add']['hostname'] = 'Hostname';
+$lang['add']['username'] = 'Username';
+$lang['add']['enc_method'] = 'Encryption method';
+$lang['add']['mins_interval'] = 'Polling interval (minutes)';
+$lang['add']['exclude'] = 'Exclude objects (regex)';
+$lang['add']['delete2duplicates'] = 'Delete duplicates on destination';
+
 $lang['add']['title'] = 'Add object';
 $lang['add']['domain'] = 'Domain';
 $lang['add']['active'] = 'Active';

+ 91 - 3
data/web/user.php

@@ -9,7 +9,6 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
 ?>
 <div class="container">
 <h3><?=$lang['user']['mailbox_settings'];?></h3>
-<p class="help-block"><?=$lang['user']['did_you_know'];?></p>
 
 <div class="panel panel-default">
 <div class="panel-heading"><?=$lang['user']['mailbox_details'];?></div>
@@ -26,13 +25,13 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
       <div class="form-group">
         <label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
         <div class="col-sm-5">
-        <input type="password" class="form-control" pattern=".{6,}" name="user_new_pass" id="user_new_pass" autocomplete="off" disabled="disabled" required>
+        <input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" disabled="disabled" required>
         </div>
       </div>
       <div class="form-group">
         <label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
         <div class="col-sm-5">
-        <input type="password" class="form-control" pattern=".{6,}" name="user_new_pass2" id="user_new_pass2" disabled="disabled" autocomplete="off" required>
+        <input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" disabled="disabled" autocomplete="off" required>
         <p class="help-block"><?=$lang['user']['new_password_description'];?></p>
         </div>
       </div>
@@ -102,6 +101,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
 	<li role="presentation" class="active"><a href="#SpamAliases" aria-controls="SpamAliases" role="tab" data-toggle="tab"><?=$lang['user']['spam_aliases'];?></a></li>
 	<li role="presentation"><a href="#Spamfilter" aria-controls="Spamfilter" role="tab" data-toggle="tab"><?=$lang['user']['spamfilter'];?></a></li>
 	<li role="presentation"><a href="#TLSPolicy" aria-controls="TLSPolicy" role="tab" data-toggle="tab"><?=$lang['user']['tls_policy'];?></a></li>
+	<li role="presentation"><a href="#Syncjobs" aria-controls="Syncjobs" role="tab" data-toggle="tab"><?=$lang['user']['sync_jobs'];?></a></li>
 </ul>
 <hr>
 
@@ -364,8 +364,96 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
 			</div>
 		</form>
 	</div>
+	<div role="tabpanel" class="tab-pane" id="Syncjobs">
+		<table class="table table-striped sortable-theme-bootstrap" data-sortable id="timelimitedaliases">
+			<thead>
+			<tr>
+				<th class="sort-table" style="min-width: 96px;">Server:Port</th>
+				<th class="sort-table" style="min-width: 96px;"><?=$lang['user']['encryption'];?></th>
+				<th class="sort-table" style="min-width: 96px;"><?=$lang['user']['username'];?></th>
+				<th class="sort-table" style="min-width: 35px;"><?=$lang['user']['excludes'];?></th>
+				<th class="sort-table" style="min-width: 35px;"><?=$lang['user']['interval'];?></th>
+				<th class="sort-table" style="min-width: 35px;"><?=$lang['user']['last_run'];?></th>
+				<th class="sort-table" style="min-width: 35px;">Log</th>
+				<th class="sort-table" style="max-width: 35px;"><?=$lang['user']['active'];?></th>
+				<th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['user']['action'];?></th>
+			</tr>
+			</thead>
+			<tbody>
+			<?php
+			try {
+				$stmt = $pdo->prepare("SELECT *, CONCAT(LEFT(`password1`, 3), '…') as `password1_short`
+						FROM `imapsync`
+							WHERE `user2` = :username");
+				$stmt->execute(array(':username' => $username));
+				$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+			}
+			catch(PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+			}
+			if(!empty($rows)):
+			while ($row = array_shift($rows)):
+			?>
+				<tr id="data">
+				<td><?=htmlspecialchars($row['host1'] . ':' . $row['port1']);?></td>
+				<td><?=htmlspecialchars($row['enc1']);?></td>
+				<td><?=htmlspecialchars($row['user1']);?></td>
+				<td><?=($row['exclude'] == '') ? '&#10008;' : $row['exclude'];?></td>
+				<td><?=htmlspecialchars($row['mins_interval']);?> min</td>
+				<td><?=(empty($row['last_run'])) ? '&#10008;' : htmlspecialchars(date($lang['user']['syncjob_full_date'], strtotime($row['last_run'])));?></td>
+				<td>
+        <?php
+        if (empty($row['returned_text'])) {
+          echo '&#10008;';
+        }
+        else {
+        ?>
+          <a href="#logModal" data-toggle="modal" data-log-text="<?=htmlspecialchars($row['returned_text']);?>">Open logs</a>
+        <?php
+        }
+        ?>
+        </td>
+				<td><?=($row['active'] == '1') ? '&#10004;' : '&#10008;';?></td>
+        <td style="text-align: right;">
+          <div class="btn-group">
+            <a href="/edit.php?syncjob=<?=urlencode($row['id']);?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['user']['edit'];?></a>
+            <a href="/delete.php?syncjob=<?=urlencode($row['id']);?>" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['user']['remove'];?></a>
+          </div>
+        </td>
+				</tr>
+			<?php
+			endwhile;
+			else:
+			?>
+				<tr id="no-data"><td colspan="9" style="text-align: center; font-style: italic;"><?=$lang['user']['no_record'];?></td></tr>
+			<?php
+			endif;	
+			?>
+			</tbody>
+      <tfoot>
+        <tr id="no-data">
+          <td colspan="9" style="text-align: center; font-style: normal; border-top: 1px solid #e7e7e7;">
+            <a href="/add.php?syncjob"><?=$lang['user']['create_syncjob'];?></a>
+          </td>
+        </tr>
+      </tfoot>
+		</table>
+		</div>
+	</div>
 </div>
 <br />
+<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
+  <div class="modal-dialog modal-lg" role="document">
+    <div class="modal-content">
+      <div class="modal-body">
+        <span id="logText"></span>
+      </div>
+    </div>
+  </div>
+</div>
 </div> <!-- /container -->
 <script src="js/sorttable.js"></script>
 <script src="js/user.js"></script>