-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ebd1820
commit 9a87af8
Showing
1 changed file
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
#!/usr/bin/env python | ||
|
||
import dataclasses | ||
from collections.abc import Iterable | ||
|
||
import click | ||
from sqlalchemy import and_, or_ | ||
|
||
from inbox.crispin import CrispinClient, writable_connection_pool | ||
from inbox.models.account import Account | ||
from inbox.models.backends.imap import ImapUid | ||
from inbox.models.folder import Folder | ||
from inbox.models.session import global_session_scope | ||
|
||
|
||
@dataclasses.dataclass | ||
class LocalAccount: | ||
id: int | ||
email: str | ||
provider: str | ||
sync_state: str | ||
|
||
|
||
def fetch_accounts( | ||
*, host: "str | None", account_id: "str | None" | ||
) -> "list[LocalAccount]": | ||
with global_session_scope() as db_session: | ||
accounts = db_session.query(Account).filter(Account.sync_state == "running") | ||
if host: | ||
process_identifier = f"{host}:0" | ||
accounts = accounts.filter( | ||
Account.sync_should_run, | ||
or_( | ||
and_( | ||
Account.desired_sync_host == process_identifier, | ||
Account.sync_host.is_(None), | ||
), | ||
and_( | ||
Account.desired_sync_host.is_(None), | ||
Account.sync_host == process_identifier, | ||
), | ||
and_( | ||
Account.desired_sync_host == process_identifier, | ||
Account.sync_host == process_identifier, | ||
), | ||
), | ||
) | ||
if account_id: | ||
accounts = accounts.filter(Account.id == account_id) | ||
|
||
return [ | ||
LocalAccount( | ||
id=account.id, | ||
email=account.email_address, | ||
provider=account.provider, | ||
sync_state=account.sync_state, | ||
) | ||
for account in accounts | ||
] | ||
|
||
|
||
@dataclasses.dataclass | ||
class ServerInfo: | ||
welcome: str | ||
capabilities: list[str] | ||
|
||
|
||
def get_server_info(crispin_client: CrispinClient, account: Account) -> ServerInfo: | ||
return ServerInfo( | ||
welcome=crispin_client.conn.welcome.decode(), | ||
capabilities=[ | ||
capability.decode() for capability in crispin_client.conn.capabilities() | ||
], | ||
) | ||
|
||
|
||
@dataclasses.dataclass | ||
class RemoteFolder: | ||
name: str | ||
role: "str | None" | ||
uidnext: int | ||
exists: int | ||
|
||
|
||
def fetch_remote_folders( | ||
provider: str, crispin_client: CrispinClient | ||
) -> Iterable[RemoteFolder]: | ||
try: | ||
folder_names = crispin_client.folder_names() | ||
except Exception: | ||
return | ||
|
||
for role, folders in folder_names.items(): | ||
if provider == "gmail" and role not in ["all", "spam", "trash"]: | ||
continue | ||
|
||
for folder in folders: | ||
try: | ||
result = crispin_client.select_folder( | ||
folder, lambda _account_id, _folder_name, select_info: select_info | ||
) | ||
except Exception: | ||
continue | ||
|
||
yield RemoteFolder( | ||
name=folder, | ||
role=role, | ||
uidnext=result[b"UIDNEXT"], | ||
exists=result[b"EXISTS"], | ||
) | ||
|
||
|
||
@dataclasses.dataclass | ||
class LocalFolder: | ||
id: int | ||
name: str | ||
state: str | ||
uidnext: int | ||
exists: int | ||
|
||
|
||
def fetch_local_folders(account: LocalAccount) -> Iterable[LocalFolder]: | ||
with global_session_scope() as db_session: | ||
for folder in db_session.query(Folder).filter(Folder.account_id == account.id): | ||
exists = ( | ||
db_session.query(ImapUid).filter(ImapUid.folder_id == folder.id).count() | ||
) | ||
uidnext = ( | ||
( | ||
db_session.query(ImapUid.msg_uid) | ||
.filter(ImapUid.folder_id == folder.id) | ||
.order_by(ImapUid.msg_uid.desc()) | ||
.limit(1) | ||
.scalar() | ||
) | ||
or 0 | ||
) + 1 | ||
yield LocalFolder( | ||
id=folder.id, | ||
name=folder.name, | ||
state=folder.imapsyncstatus.state, | ||
uidnext=uidnext, | ||
exists=exists, | ||
) | ||
|
||
|
||
@click.command() | ||
@click.option("--host", default=None) | ||
@click.option("--account-id", default=None) | ||
@click.option("--include-server-info", is_flag=True) | ||
def main(host: "str | None", account_id: "str | None", include_server_info: bool): | ||
accounts = fetch_accounts(host=host, account_id=account_id) | ||
total_remote_exists = 0 | ||
total_local_exists = 0 | ||
for account in accounts: | ||
print(account) | ||
|
||
try: | ||
with writable_connection_pool(account.id).get() as crispin_client: | ||
if include_server_info: | ||
server_info = get_server_info(crispin_client, account) | ||
print("\t", server_info) | ||
print() | ||
|
||
total_folder_remote_exists = 0 | ||
for remote_folder in fetch_remote_folders( | ||
account.provider, crispin_client | ||
): | ||
print("\t", remote_folder) | ||
total_folder_remote_exists += remote_folder.exists | ||
total_remote_exists += remote_folder.exists | ||
print("\t Total remote EXISTS:", total_folder_remote_exists) | ||
print() | ||
|
||
total_folder_local_exists = 0 | ||
for local_folder in fetch_local_folders(account): | ||
print("\t", local_folder) | ||
total_folder_local_exists += local_folder.exists | ||
total_local_exists += local_folder.exists | ||
print("\t Total local EXISTS:", total_folder_local_exists) | ||
print( | ||
"\t Total difference:", | ||
total_folder_remote_exists - total_folder_local_exists, | ||
) | ||
print() | ||
except Exception as e: | ||
print("\t Exception opening the connection", e) | ||
print() | ||
|
||
print("Total remote EXISTS:", total_remote_exists) | ||
print("Total local EXISTS:", total_local_exists) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |