-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimap_client.py
More file actions
257 lines (213 loc) Β· 11.4 KB
/
Copy pathimap_client.py
File metadata and controls
257 lines (213 loc) Β· 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/usr/bin/env python3
"""
IMAP client for Gmail to IMAP transfer system.
"""
import time
import logging
from datetime import datetime
from typing import List
# IMAP imports
import imapclient
class IMAPClient:
"""Handles IMAP server operations with SSL stability."""
def __init__(self, server: str, port: int, username: str, password: str, use_ssl: bool = True):
self.server = server
self.port = port
self.username = username
self.password = password
self.use_ssl = use_ssl
self.client = None
self.connection_start_time = None
self.connection_errors = 0
self.last_activity = None
self.total_uploads = 0
self.max_connection_duration = 900 # 15 minutes max connection time
self.max_uploads_per_connection = 100 # Max uploads before reconnect
self.connect()
def connect(self) -> None:
"""Connect to IMAP server with health monitoring."""
try:
self.connection_start_time = time.time()
logging.info(f"π Attempting IMAP connection to {self.server}:{self.port}")
self.client = imapclient.IMAPClient(self.server, port=self.port, ssl=self.use_ssl)
self.client.login(self.username, self.password)
self.last_activity = time.time()
logging.info(f"β
Connected to IMAP server {self.server}")
logging.info(f"π Connection established in {self.last_activity - self.connection_start_time:.2f}s")
# Diagnostic: Check server capabilities and namespaces
try:
capabilities = self.client.capabilities()
logging.info(f"IMAP server capabilities: {list(capabilities)}")
# Check namespace support
if b'NAMESPACE' in capabilities:
namespace = self.client.namespace()
logging.info(f"IMAP namespaces: {namespace}")
# Extract personal namespace prefix
if namespace and namespace[0]:
personal_ns = namespace[0][0]
if personal_ns:
self.namespace_prefix = personal_ns[0] if personal_ns[0] else ""
self.namespace_delimiter = personal_ns[1] if personal_ns[1] else "."
logging.info(f"Personal namespace prefix: '{self.namespace_prefix}', delimiter: '{self.namespace_delimiter}'")
else:
self.namespace_prefix = ""
self.namespace_delimiter = "."
else:
self.namespace_prefix = ""
self.namespace_delimiter = "."
else:
logging.warning("Server does not support NAMESPACE command")
# Default assumption for most IMAP servers
self.namespace_prefix = "INBOX."
self.namespace_delimiter = "."
except Exception as e:
logging.warning(f"Could not get namespace info: {e}")
# Default assumption for most IMAP servers that require INBOX prefix
self.namespace_prefix = "INBOX."
self.namespace_delimiter = "."
logging.info(f"Using namespace prefix: '{self.namespace_prefix}' with delimiter: '{self.namespace_delimiter}'")
except Exception as e:
logging.error(f"Failed to connect to IMAP server: {e}")
raise
def create_folder(self, folder_name: str) -> None:
"""Create folder if it doesn't exist."""
try:
# Apply namespace prefix if needed
full_folder_name = self._get_full_folder_name(folder_name)
if not self.client.folder_exists(full_folder_name):
self.client.create_folder(full_folder_name)
logging.info(f"Created IMAP folder: {full_folder_name}")
else:
logging.info(f"IMAP folder already exists: {full_folder_name}")
except Exception as e:
logging.error(f"Failed to create folder {folder_name} (full name: {self._get_full_folder_name(folder_name)}): {e}")
raise
def _get_full_folder_name(self, folder_name: str) -> str:
"""Get full folder name with namespace prefix."""
# Don't prefix INBOX itself
if folder_name.upper() == 'INBOX':
return 'INBOX'
# If folder already has the prefix, don't add it again
if hasattr(self, 'namespace_prefix') and self.namespace_prefix:
if folder_name.startswith(self.namespace_prefix):
return folder_name
return f"{self.namespace_prefix}{folder_name}"
else:
# Default behavior for servers without namespace info
if folder_name.startswith('INBOX.'):
return folder_name
return f"INBOX.{folder_name}"
def upload_message(self, folder_name: str, message_data: bytes, flags: List[str] = None, msg_time: datetime = None) -> None:
"""Upload message to IMAP folder with SSL stability and connection recycling."""
max_retries = 3
for attempt in range(max_retries):
try:
if flags is None:
flags = []
# Check if connection needs recycling BEFORE upload
if self._should_recycle_connection():
logging.info("π Recycling IMAP connection for SSL stability")
self._reconnect()
# Check connection health before upload
self._check_connection_health()
# Apply namespace prefix if needed
full_folder_name = self._get_full_folder_name(folder_name)
# Track activity
start_time = time.time()
self.client.append(full_folder_name, message_data, flags, msg_time)
self.last_activity = time.time()
self.total_uploads += 1
# Log slow uploads
upload_time = self.last_activity - start_time
if upload_time > 5.0: # More than 5 seconds
logging.warning(f"β οΈ Slow IMAP upload: {upload_time:.2f}s for message to {folder_name}")
return # Success - exit retry loop
except Exception as e:
self.connection_errors += 1
full_folder_name = self._get_full_folder_name(folder_name)
# Check if this is an SSL/connection error that should trigger reconnection
is_ssl_error = ("SSL" in str(e) or "socket" in str(e).lower() or
"LOGOUT" in str(e) or "connection" in str(e).lower())
if is_ssl_error:
logging.error(f"π IMAP connection error #{self.connection_errors}: {e}")
self._log_connection_diagnostics()
# Try to reconnect for SSL errors (except on last attempt)
if attempt < max_retries - 1:
logging.info(f"π Attempting reconnection (attempt {attempt + 1}/{max_retries})")
try:
self._reconnect()
time.sleep(1) # Brief pause before retry
continue
except Exception as reconnect_error:
logging.error(f"β Reconnection failed: {reconnect_error}")
else:
logging.error(f"Failed to upload message to {folder_name} (full name: {full_folder_name}): {e}")
# If this is the last attempt or not an SSL error, re-raise
if attempt == max_retries - 1:
raise
def _should_recycle_connection(self) -> bool:
"""Check if connection should be recycled for SSL stability."""
if not self.connection_start_time:
return False
# Check connection duration
connection_age = time.time() - self.connection_start_time
if connection_age > self.max_connection_duration:
logging.info(f"π Connection recycling: age {connection_age:.1f}s > {self.max_connection_duration}s")
return True
# Check upload count
if self.total_uploads >= self.max_uploads_per_connection:
logging.info(f"π Connection recycling: {self.total_uploads} uploads >= {self.max_uploads_per_connection}")
return True
# Check error rate
if self.connection_errors >= 10: # Too many errors
logging.info(f"β Connection recycling: {self.connection_errors} errors")
return True
return False
def _reconnect(self) -> None:
"""Safely reconnect to IMAP server."""
try:
# Close existing connection
if self.client:
try:
self.client.logout()
except:
pass # Ignore errors on logout
self.client = None
# Reset counters
old_errors = self.connection_errors
self.connection_errors = 0
self.total_uploads = 0
# Reconnect
self.connect()
logging.info(f"β
IMAP reconnection successful (previous errors: {old_errors})")
except Exception as e:
logging.error(f"β IMAP reconnection failed: {e}")
raise
def _check_connection_health(self) -> None:
"""Check IMAP connection health."""
if self.last_activity:
time_since_activity = time.time() - self.last_activity
if time_since_activity > 300: # 5 minutes of inactivity
logging.warning(f"β οΈ IMAP connection inactive for {time_since_activity:.1f}s")
def _log_connection_diagnostics(self) -> None:
"""Log detailed connection diagnostics."""
if self.connection_start_time:
connection_duration = time.time() - self.connection_start_time
logging.info(f"π Connection duration: {connection_duration:.1f}s")
logging.info(f"β Connection errors: {self.connection_errors}")
if self.last_activity:
time_since_activity = time.time() - self.last_activity
logging.info(f"β±οΈ Time since last activity: {time_since_activity:.1f}s")
def disconnect(self) -> None:
"""Disconnect from IMAP server with diagnostics."""
if self.client:
try:
self.client.logout()
if self.connection_start_time:
total_duration = time.time() - self.connection_start_time
logging.info(f"β
Disconnected from IMAP server (duration: {total_duration:.1f}s, errors: {self.connection_errors})")
else:
logging.info("β
Disconnected from IMAP server")
except Exception as e:
logging.error(f"β Error disconnecting from IMAP server: {e}")
self._log_connection_diagnostics()