Skip to content

Commit dc741cc

Browse files
committed
inbox-summary #notests
1 parent c66d909 commit dc741cc

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

bin/inbox-summary.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
#!/usr/bin/env python
2+
3+
import dataclasses
4+
from collections.abc import Iterable
5+
6+
import click
7+
from sqlalchemy import and_, or_
8+
9+
from inbox.crispin import CrispinClient, writable_connection_pool
10+
from inbox.models.account import Account
11+
from inbox.models.backends.imap import ImapUid
12+
from inbox.models.folder import Folder
13+
from inbox.models.session import global_session_scope
14+
15+
16+
@dataclasses.dataclass
17+
class LocalAccount:
18+
id: int
19+
email: str
20+
provider: str
21+
sync_state: str
22+
23+
24+
def fetch_accounts(
25+
*, host: "str | None", account_id: "str | None"
26+
) -> "list[LocalAccount]":
27+
with global_session_scope() as db_session:
28+
accounts = db_session.query(Account).filter(Account.sync_state == "running")
29+
if host:
30+
process_identifier = f"{host}:0"
31+
accounts = accounts.filter(
32+
Account.sync_should_run,
33+
or_(
34+
and_(
35+
Account.desired_sync_host == process_identifier,
36+
Account.sync_host.is_(None),
37+
),
38+
and_(
39+
Account.desired_sync_host.is_(None),
40+
Account.sync_host == process_identifier,
41+
),
42+
and_(
43+
Account.desired_sync_host == process_identifier,
44+
Account.sync_host == process_identifier,
45+
),
46+
),
47+
)
48+
if account_id:
49+
accounts = accounts.filter(Account.id == account_id)
50+
51+
return [
52+
LocalAccount(
53+
id=account.id,
54+
email=account.email_address,
55+
provider=account.provider,
56+
sync_state=account.sync_state,
57+
)
58+
for account in accounts
59+
]
60+
61+
62+
@dataclasses.dataclass
63+
class ServerInfo:
64+
welcome: str
65+
capabilities: list[str]
66+
67+
68+
def get_server_info(crispin_client: CrispinClient, account: Account) -> ServerInfo:
69+
return ServerInfo(
70+
welcome=crispin_client.conn.welcome.decode(),
71+
capabilities=[
72+
capability.decode() for capability in crispin_client.conn.capabilities()
73+
],
74+
)
75+
76+
77+
@dataclasses.dataclass
78+
class RemoteFolder:
79+
name: str
80+
role: "str | None"
81+
uidnext: int
82+
exists: int
83+
84+
85+
def fetch_remote_folders(
86+
provider: str, crispin_client: CrispinClient
87+
) -> Iterable[RemoteFolder]:
88+
try:
89+
folders = crispin_client.folders()
90+
except Exception:
91+
return
92+
93+
for folder in sorted(folders, key=lambda f: f.display_name):
94+
if provider == "gmail" and folder.role not in ["all", "spam", "trash"]:
95+
continue
96+
97+
try:
98+
result = crispin_client.select_folder(
99+
folder.display_name,
100+
lambda _account_id, _folder_name, select_info: select_info,
101+
)
102+
except Exception:
103+
continue
104+
105+
yield RemoteFolder(
106+
name=folder.display_name,
107+
role=folder.role,
108+
uidnext=result[b"UIDNEXT"],
109+
exists=result[b"EXISTS"],
110+
)
111+
112+
113+
@dataclasses.dataclass
114+
class LocalFolder:
115+
id: int
116+
name: str
117+
state: str
118+
uidmax: int
119+
exists: int
120+
121+
122+
def fetch_local_folders(account: LocalAccount) -> Iterable[LocalFolder]:
123+
with global_session_scope() as db_session:
124+
for folder in (
125+
db_session.query(Folder)
126+
.filter(Folder.account_id == account.id)
127+
.order_by(Folder.name)
128+
):
129+
exists = (
130+
db_session.query(ImapUid).filter(ImapUid.folder_id == folder.id).count()
131+
)
132+
uidmax = (
133+
db_session.query(ImapUid.msg_uid)
134+
.filter(ImapUid.folder_id == folder.id)
135+
.order_by(ImapUid.msg_uid.desc())
136+
.limit(1)
137+
.scalar()
138+
) or 0
139+
yield LocalFolder(
140+
id=folder.id,
141+
name=folder.name,
142+
state=folder.imapsyncstatus.state,
143+
uidmax=uidmax,
144+
exists=exists,
145+
)
146+
147+
148+
@dataclasses.dataclass
149+
class SummarizedList:
150+
value: list
151+
max_values: int = 10
152+
153+
def __repr__(self):
154+
if len(self.value) <= self.max_values:
155+
return repr(self.value)
156+
157+
return f"[{self.value[0]}, ... ,{self.value[-1]} len={len(self.value)}]"
158+
159+
160+
@dataclasses.dataclass
161+
class LocalFolderDiff:
162+
name: str
163+
uids_to_add: list[int]
164+
uids_to_delete: list[int]
165+
166+
167+
@dataclasses.dataclass
168+
class LocalFolderMissing:
169+
name: str
170+
171+
172+
def compare_local_and_remote(
173+
crispin_client: CrispinClient,
174+
remote_folders: list[RemoteFolder],
175+
local_folders: list[LocalFolder],
176+
):
177+
remote_folders_by_name = {folder.name: folder for folder in remote_folders}
178+
local_folders_by_name = {folder.name: folder for folder in local_folders}
179+
180+
for name, remote_folder in remote_folders_by_name.items():
181+
local_folder = local_folders_by_name.get(name)
182+
if not local_folder:
183+
yield LocalFolderMissing(name=name)
184+
185+
if local_folder.exists == remote_folder.exists:
186+
continue
187+
188+
crispin_client.select_folder(
189+
local_folder.name,
190+
lambda _account_id, _folder_name, select_info: select_info,
191+
)
192+
remote_uids = set(crispin_client.all_uids())
193+
with global_session_scope() as db_session:
194+
local_uids = set(
195+
uid
196+
for uid, in db_session.query(ImapUid.msg_uid).filter(
197+
ImapUid.folder_id == local_folder.id
198+
)
199+
)
200+
201+
uids_to_add = remote_uids - local_uids
202+
uids_to_delete = local_uids - remote_uids
203+
204+
yield LocalFolderDiff(
205+
name=local_folder.name,
206+
uids_to_add=SummarizedList(sorted(uids_to_add)),
207+
uids_to_delete=SummarizedList(sorted(uids_to_delete)),
208+
)
209+
210+
211+
@click.command()
212+
@click.option("--host", default=None)
213+
@click.option("--account-id", default=None)
214+
@click.option("--include-server-info", is_flag=True)
215+
def main(host: "str | None", account_id: "str | None", include_server_info: bool):
216+
accounts = fetch_accounts(host=host, account_id=account_id)
217+
total_remote_exists = 0
218+
total_local_exists = 0
219+
for account in accounts:
220+
print(account)
221+
222+
try:
223+
with writable_connection_pool(account.id).get() as crispin_client:
224+
if include_server_info:
225+
server_info = get_server_info(crispin_client, account)
226+
print("\t", server_info)
227+
print()
228+
229+
total_folder_remote_exists = 0
230+
remote_folders = []
231+
for remote_folder in fetch_remote_folders(
232+
account.provider, crispin_client
233+
):
234+
print("\t", remote_folder)
235+
remote_folders.append(remote_folder)
236+
total_folder_remote_exists += remote_folder.exists
237+
total_remote_exists += remote_folder.exists
238+
print("\t Total remote EXISTS:", total_folder_remote_exists)
239+
print()
240+
241+
total_folder_local_exists = 0
242+
local_folders = []
243+
for local_folder in fetch_local_folders(account):
244+
print("\t", local_folder)
245+
local_folders.append(local_folder)
246+
total_folder_local_exists += local_folder.exists
247+
total_local_exists += local_folder.exists
248+
print("\t Total local EXISTS:", total_folder_local_exists)
249+
print(
250+
"\t Total difference:",
251+
total_folder_remote_exists - total_folder_local_exists,
252+
)
253+
print()
254+
255+
for diff in compare_local_and_remote(
256+
crispin_client, remote_folders, local_folders
257+
):
258+
print("\t", diff)
259+
print()
260+
except Exception as e:
261+
print("\t Exception opening the connection", e)
262+
print()
263+
264+
print("Total remote EXISTS:", total_remote_exists)
265+
print("Total local EXISTS:", total_local_exists)
266+
267+
268+
if __name__ == "__main__":
269+
main()

0 commit comments

Comments
 (0)