Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sync/db/003_folders.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ CREATE TABLE IF NOT EXISTS `folders` (
`name` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`count` int(10) unsigned DEFAULT '0',
`synced` int(10) unsigned DEFAULT '0',
`sync_status` ENUM('not_synced','syncing', 'syncing_need_resync','synced_need_resync', 'synced','error') NOT NULL DEFAULT 'not_synced',
`sync_host` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`sync_pid` int(11) DEFAULT NULL,
`synced_at` timestamp NULL DEFAULT NULL,
`uid_validity` int(10) unsigned DEFAULT '0',
`deleted` tinyint(1) unsigned DEFAULT '0',
`ignored` tinyint(1) unsigned DEFAULT '0',
Expand Down
57 changes: 57 additions & 0 deletions sync/src/Enum/FolderSyncStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\Enum;

class FolderSyncStatus {

const __default = self::NOT_SYNCED;

/**
* Initial state, when folder just added to database and syncing process has never processed.
*/
const NOT_SYNCED = 'not_synced';

/**
* This folder is syncing now.
*/
const SYNCING = 'syncing';

/**
* This folder is syncing now, and we received one or more request for resync after syncing process started.
* Example: syncing process started and user delete / change flag of message, which already synced.
* We can not miss this event and resyncing needed.
*/
const SYNCING_NEED_RESYNC = 'syncing_need_resync';

/**
* This folder is just synced, another folder of the same account is syncing now.
* That means, that sync is running now for this account.
* After account sync is complete we need to resync this folder, because watcher tell us, that folder is changed.
*/
const SYNCED_NEED_RESYNC = 'synced_need_resync';

/**
* Sync process completed and there is no new events received from imap server ("mail" / "update" / "purge").
*/
const SYNCED = 'synced';

/**
* There was some error, during sync process.
*/
const ERROR = 'error';

protected static $choices = [
self::NOT_SYNCED => 'Not synced',
self::SYNCING => 'Syncing',
self::SYNCING_NEED_RESYNC => 'Syncing but need resync',
self::SYNCED_NEED_RESYNC => 'Synced but need resync',
self::SYNCED => 'Synced',
self::ERROR => 'Error'
];


public static function getValues(): array
{
return \array_keys(static::$choices);
}
}
52 changes: 52 additions & 0 deletions sync/src/Model/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Model;

use Fn;
use App\Enum\FolderSyncStatus;
use PDO;
use DateTime;
use App\Model;
Expand All @@ -26,6 +27,10 @@ class Folder extends Model
public $account_id;
public $created_at;
public $uid_validity;
public $sync_status;
public $sync_host;
public $sync_pid;
public $synced_at;

const DRAFTS = [
'[Gmail]/Drafts',
Expand Down Expand Up @@ -339,4 +344,51 @@ public function getSentByAccount(int $accountId)

throw new NotFoundException('sent mail folder');
}

/**
* @param string $status
* @param string $host
* @param int $pid
* @return int
* @throws DatabaseUpdateException
*/
public function updateSyncData(string $status, string $host, int $pid)
{
if (!in_array($status, FolderSyncStatus::getValues())) {
throw new \LogicException("Unsupported folder sync status '{$status}'");
}
$data = [
'sync_status' => $status,
'sync_host' => $host,
'sync_pid' => $pid
];
if ($status == FolderSyncStatus::SYNCED) {
// In UTC
$now = new \DateTime();
$date = date('Y-m-d H:i:s', $now->getTimestamp());
$data['synced_at'] = $date;
}
$updated = $this->db()
->update($data)
->table('folders')
->where('id', '=', $this->getId())
->execute();
$this->errorHandle($updated);
return $updated;

}

/**
* @return string
*/
public function getActualSyncStatus()
{
$row = $this->db()
->select(['sync_status'])
->from('folders')
->where('id', '=', $this->getId())
->execute()
->fetch();
return $row ? $row['sync_status'] : FolderSyncStatus::__default;
}
}
15 changes: 0 additions & 15 deletions sync/src/Model/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -1051,19 +1051,4 @@ private function formatAttachments(array $attachments)

return json_encode($formatted, JSON_UNESCAPED_SLASHES);
}

/**
* @param bool | int $updated Response from update operation
*
* @throws DatabaseUpdateException
*/
private function errorHandle($updated)
{
if (! Belt::isNumber($updated)) {
throw new DatabaseUpdateException(
MESSAGE,
$this->getError()
);
}
}
}
43 changes: 43 additions & 0 deletions sync/src/Sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace App;

use Fn;
use App\Enum\FolderSyncStatus;
use DateTime;
use Exception;
use PDOException;
Expand Down Expand Up @@ -616,6 +617,7 @@ private function syncMessages(AccountModel $account, array $folders, array $opti
$this->log->debug('Syncing messages in each folder');
}

/** @var FolderModel $folder */
foreach ($folders as $folder) {
$this->retriesMessages[$account->email] = 1;
$this->stats->setActiveFolder($folder->name);
Expand Down Expand Up @@ -708,15 +710,56 @@ private function syncFolderMessages(
// Select the folder's mailbox, this is sent to the
// messages sync library to perform operations on
$selectStats = $this->mailbox->select($folder->name);
if (! Fn\get($options, Sync::OPT_SKIP_DOWNLOAD)) {
$folder->updateSyncData(
FolderSyncStatus::SYNCING,
gethostname(),
getmypid()
);
}
$messageSync->run($account, $folder, $selectStats, $options);
/**
* If watcher received imap event and mark this folder to resync
* after sync is started => make sync one more time.
*/
if ($folder->getActualSyncStatus() == FolderSyncStatus::SYNCING_NEED_RESYNC) {
$messageSync->run($account, $folder, $selectStats, $options);
}
if (! Fn\get($options, Sync::OPT_SKIP_DOWNLOAD)) {
$folder->updateSyncData(
FolderSyncStatus::SYNCED,
gethostname(),
getmypid()
);
}
$this->checkForHalt();
} catch (PDOException $e) {
$folder->updateSyncData(
FolderSyncStatus::ERROR,
gethostname(),
getmypid()
);
throw $e;
} catch (StopException $e) {
$folder->updateSyncData(
FolderSyncStatus::ERROR,
gethostname(),
getmypid()
);
throw $e;
} catch (TerminateException $e) {
$folder->updateSyncData(
FolderSyncStatus::ERROR,
gethostname(),
getmypid()
);
throw $e;
} catch (Exception $e) {
$folder->updateSyncData(
FolderSyncStatus::ERROR,
gethostname(),
getmypid()
);
$this->stats->unsetActiveFolder();
$this->log->error(substr($e->getMessage(), 0, 500));
$this->checkForClosedConnection($e);
Expand Down
7 changes: 6 additions & 1 deletion sync/src/Sync/Folders.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace App\Sync;

use Fn;
use App\Enum\FolderSyncStatus;
use App\Sync;
use Monolog\Logger;
use League\CLImate\CLImate;
Expand Down Expand Up @@ -96,7 +97,11 @@ private function addNewFolders(
$folder = new FolderModel([
'name' => $folderName,
'account_id' => $account->getId(),
'ignored' => $this->getIgnored($folderName)
'ignored' => $this->getIgnored($folderName),
'sync_status' => FolderSyncStatus::NOT_SYNCED,
'sync_host' => NULL,
'sync_pid' => NULL,
'synced_at' => NULL
]);

$folder->save();
Expand Down
16 changes: 16 additions & 0 deletions sync/src/Traits/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Traits;

use App\Exceptions\DatabaseUpdate as DatabaseUpdateException;
use Belt\Belt;
use App\Exceptions\NotFound as NotFoundException;
use App\Exceptions\Validation as ValidationException;
Expand Down Expand Up @@ -150,4 +151,19 @@ private function updateUtf8Values(array &$data, array $keys)
$data[$key] = @iconv('UTF-8', 'UTF-8//IGNORE', $data[$key]);
}
}

/**
* @param bool | int $updated Response from update operation
*
* @throws DatabaseUpdateException
*/
protected function errorHandle($updated)
{
if (! Belt::isNumber($updated)) {
throw new DatabaseUpdateException(
MESSAGE,
$this->getError()
);
}
}
}