ans123 commited on
Commit
f3f5410
Β·
verified Β·
1 Parent(s): 5b17247

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +659 -0
app.py ADDED
@@ -0,0 +1,659 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio Email Client App - Web interface for email fetching and management
4
+ Supports IMAP and POP3 protocols with secure authentication
5
+ """
6
+
7
+ import gradio as gr
8
+ import imaplib
9
+ import poplib
10
+ import email
11
+ from email.header import decode_header
12
+ from email.utils import parsedate_to_datetime
13
+ import json
14
+ import logging
15
+ from datetime import datetime
16
+ import pandas as pd
17
+ from typing import List, Dict, Tuple, Optional
18
+ import re
19
+
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger("gradio-email-app")
23
+
24
+ class EmailClient:
25
+ """Email client supporting both IMAP and POP3"""
26
+
27
+ def __init__(self):
28
+ self.imap_conn = None
29
+ self.pop_conn = None
30
+ self.protocol = None
31
+ self.connected_email = None
32
+
33
+ def connect_imap(self, server: str, port: int, email_addr: str, password: str, use_ssl: bool = True):
34
+ """Connect to IMAP server"""
35
+ try:
36
+ if use_ssl:
37
+ self.imap_conn = imaplib.IMAP4_SSL(server, port)
38
+ else:
39
+ self.imap_conn = imaplib.IMAP4(server, port)
40
+
41
+ self.imap_conn.login(email_addr, password)
42
+ self.protocol = "IMAP"
43
+ self.connected_email = email_addr
44
+ logger.info(f"Connected to IMAP server: {server}")
45
+ return True, f"βœ… Connected to IMAP server: {server}"
46
+ except Exception as e:
47
+ logger.error(f"IMAP connection failed: {e}")
48
+ return False, f"❌ IMAP connection failed: {str(e)}"
49
+
50
+ def connect_pop3(self, server: str, port: int, email_addr: str, password: str, use_ssl: bool = True):
51
+ """Connect to POP3 server"""
52
+ try:
53
+ if use_ssl:
54
+ self.pop_conn = poplib.POP3_SSL(server, port)
55
+ else:
56
+ self.pop_conn = poplib.POP3(server, port)
57
+
58
+ self.pop_conn.user(email_addr)
59
+ self.pop_conn.pass_(password)
60
+ self.protocol = "POP3"
61
+ self.connected_email = email_addr
62
+ logger.info(f"Connected to POP3 server: {server}")
63
+ return True, f"βœ… Connected to POP3 server: {server}"
64
+ except Exception as e:
65
+ logger.error(f"POP3 connection failed: {e}")
66
+ return False, f"❌ POP3 connection failed: {str(e)}"
67
+
68
+ def get_folders(self) -> Tuple[List[str], str]:
69
+ """Get available folders (IMAP only)"""
70
+ if self.protocol != "IMAP" or not self.imap_conn:
71
+ return [], "❌ Folders are only available with IMAP connections"
72
+
73
+ try:
74
+ status, folders = self.imap_conn.list()
75
+ folder_list = []
76
+ for folder in folders:
77
+ # Parse folder name from IMAP response
78
+ parts = folder.decode().split('"')
79
+ if len(parts) >= 3:
80
+ folder_name = parts[-2]
81
+ folder_list.append(folder_name)
82
+ return folder_list, f"βœ… Found {len(folder_list)} folders"
83
+ except Exception as e:
84
+ logger.error(f"Failed to get folders: {e}")
85
+ return [], f"❌ Failed to get folders: {str(e)}"
86
+
87
+ def fetch_emails(self, folder: str = "INBOX", limit: int = 10, search_criteria: str = "ALL") -> Tuple[List[Dict], str]:
88
+ """Fetch emails from specified folder"""
89
+ if self.protocol == "IMAP":
90
+ return self._fetch_emails_imap(folder, limit, search_criteria)
91
+ elif self.protocol == "POP3":
92
+ return self._fetch_emails_pop3(limit)
93
+ else:
94
+ return [], "❌ Not connected to any email server"
95
+
96
+ def _fetch_emails_imap(self, folder: str, limit: int, search_criteria: str) -> Tuple[List[Dict], str]:
97
+ """Fetch emails using IMAP"""
98
+ if not self.imap_conn:
99
+ return [], "❌ No IMAP connection"
100
+
101
+ try:
102
+ self.imap_conn.select(folder)
103
+ status, messages = self.imap_conn.search(None, search_criteria)
104
+
105
+ if status != 'OK':
106
+ return [], f"❌ Search failed in folder {folder}"
107
+
108
+ email_ids = messages[0].split()
109
+ email_ids = email_ids[-limit:] if len(email_ids) > limit else email_ids
110
+
111
+ emails = []
112
+ for email_id in reversed(email_ids):
113
+ try:
114
+ status, msg_data = self.imap_conn.fetch(email_id, '(RFC822)')
115
+ if status == 'OK':
116
+ email_body = msg_data[0][1]
117
+ email_message = email.message_from_bytes(email_body)
118
+ parsed_email = self._parse_email(email_message)
119
+ parsed_email['id'] = email_id.decode()
120
+ emails.append(parsed_email)
121
+ except Exception as e:
122
+ logger.error(f"Failed to fetch email {email_id}: {e}")
123
+ continue
124
+
125
+ return emails, f"βœ… Fetched {len(emails)} emails from {folder}"
126
+ except Exception as e:
127
+ logger.error(f"IMAP fetch failed: {e}")
128
+ return [], f"❌ IMAP fetch failed: {str(e)}"
129
+
130
+ def _fetch_emails_pop3(self, limit: int) -> Tuple[List[Dict], str]:
131
+ """Fetch emails using POP3"""
132
+ if not self.pop_conn:
133
+ return [], "❌ No POP3 connection"
134
+
135
+ try:
136
+ num_messages = len(self.pop_conn.list()[1])
137
+ start_index = max(1, num_messages - limit + 1)
138
+
139
+ emails = []
140
+ for i in range(start_index, num_messages + 1):
141
+ try:
142
+ raw_email = b'\n'.join(self.pop_conn.retr(i)[1])
143
+ email_message = email.message_from_bytes(raw_email)
144
+ parsed_email = self._parse_email(email_message)
145
+ parsed_email['id'] = str(i)
146
+ emails.append(parsed_email)
147
+ except Exception as e:
148
+ logger.error(f"Failed to fetch email {i}: {e}")
149
+ continue
150
+
151
+ return list(reversed(emails)), f"βœ… Fetched {len(emails)} emails"
152
+ except Exception as e:
153
+ logger.error(f"POP3 fetch failed: {e}")
154
+ return [], f"❌ POP3 fetch failed: {str(e)}"
155
+
156
+ def _parse_email(self, email_message) -> Dict:
157
+ """Parse email message into dictionary"""
158
+ def decode_mime_words(s):
159
+ if s is None:
160
+ return ""
161
+ decoded_parts = decode_header(s)
162
+ decoded_string = ""
163
+ for part, encoding in decoded_parts:
164
+ if isinstance(part, bytes):
165
+ if encoding:
166
+ decoded_string += part.decode(encoding)
167
+ else:
168
+ decoded_string += part.decode('utf-8', errors='ignore')
169
+ else:
170
+ decoded_string += part
171
+ return decoded_string
172
+
173
+ # Extract basic headers
174
+ subject = decode_mime_words(email_message.get('Subject', ''))
175
+ from_addr = decode_mime_words(email_message.get('From', ''))
176
+ to_addr = decode_mime_words(email_message.get('To', ''))
177
+ date_str = email_message.get('Date', '')
178
+
179
+ # Parse date
180
+ try:
181
+ date_obj = parsedate_to_datetime(date_str)
182
+ formatted_date = date_obj.strftime("%Y-%m-%d %H:%M:%S")
183
+ except:
184
+ formatted_date = date_str
185
+
186
+ # Extract body
187
+ body = self._extract_body(email_message)
188
+
189
+ # Extract attachments info
190
+ attachments = self._extract_attachments_info(email_message)
191
+
192
+ return {
193
+ 'subject': subject,
194
+ 'from': from_addr,
195
+ 'to': to_addr,
196
+ 'date': formatted_date,
197
+ 'body_text': body.get('text', ''),
198
+ 'body_html': body.get('html', ''),
199
+ 'attachments': attachments,
200
+ 'attachment_count': len(attachments)
201
+ }
202
+
203
+ def _extract_body(self, email_message) -> Dict[str, str]:
204
+ """Extract email body (text and HTML)"""
205
+ body = {'text': '', 'html': ''}
206
+
207
+ if email_message.is_multipart():
208
+ for part in email_message.walk():
209
+ content_type = part.get_content_type()
210
+ content_disposition = str(part.get('Content-Disposition', ''))
211
+
212
+ if 'attachment' not in content_disposition:
213
+ if content_type == 'text/plain':
214
+ try:
215
+ body['text'] = part.get_payload(decode=True).decode('utf-8', errors='ignore')
216
+ except:
217
+ body['text'] = str(part.get_payload())
218
+ elif content_type == 'text/html':
219
+ try:
220
+ body['html'] = part.get_payload(decode=True).decode('utf-8', errors='ignore')
221
+ except:
222
+ body['html'] = str(part.get_payload())
223
+ else:
224
+ content_type = email_message.get_content_type()
225
+ try:
226
+ payload = email_message.get_payload(decode=True).decode('utf-8', errors='ignore')
227
+ except:
228
+ payload = str(email_message.get_payload())
229
+
230
+ if content_type == 'text/plain':
231
+ body['text'] = payload
232
+ elif content_type == 'text/html':
233
+ body['html'] = payload
234
+ else:
235
+ body['text'] = payload
236
+
237
+ return body
238
+
239
+ def _extract_attachments_info(self, email_message) -> List[str]:
240
+ """Extract attachment information"""
241
+ attachments = []
242
+
243
+ if email_message.is_multipart():
244
+ for part in email_message.walk():
245
+ content_disposition = str(part.get('Content-Disposition', ''))
246
+ if 'attachment' in content_disposition:
247
+ filename = part.get_filename()
248
+ if filename:
249
+ filename = decode_header(filename)[0][0]
250
+ if isinstance(filename, bytes):
251
+ filename = filename.decode('utf-8', errors='ignore')
252
+ attachments.append(filename)
253
+
254
+ return attachments
255
+
256
+ def disconnect(self):
257
+ """Disconnect from email server"""
258
+ try:
259
+ if self.imap_conn:
260
+ self.imap_conn.close()
261
+ self.imap_conn.logout()
262
+ self.imap_conn = None
263
+ if self.pop_conn:
264
+ self.pop_conn.quit()
265
+ self.pop_conn = None
266
+ self.protocol = None
267
+ self.connected_email = None
268
+ logger.info("Disconnected from email server")
269
+ return "βœ… Disconnected from email server"
270
+ except Exception as e:
271
+ logger.error(f"Disconnect error: {e}")
272
+ return f"❌ Disconnect error: {str(e)}"
273
+
274
+ # Global email client instance
275
+ email_client = EmailClient()
276
+
277
+ # Email server configurations
278
+ EMAIL_SERVERS = {
279
+ 'Gmail': {
280
+ 'imap': {'server': 'imap.gmail.com', 'port': 993},
281
+ 'pop3': {'server': 'pop.gmail.com', 'port': 995}
282
+ },
283
+ 'Outlook/Hotmail': {
284
+ 'imap': {'server': 'outlook.office365.com', 'port': 993},
285
+ 'pop3': {'server': 'outlook.office365.com', 'port': 995}
286
+ },
287
+ 'Yahoo': {
288
+ 'imap': {'server': 'imap.mail.yahoo.com', 'port': 993},
289
+ 'pop3': {'server': 'pop.mail.yahoo.com', 'port': 995}
290
+ },
291
+ 'iCloud': {
292
+ 'imap': {'server': 'imap.mail.me.com', 'port': 993},
293
+ 'pop3': {'server': 'pop.mail.me.com', 'port': 995}
294
+ }
295
+ }
296
+
297
+ def connect_to_email(email_addr, password, provider, protocol, custom_server="", custom_port=993):
298
+ """Connect to email server"""
299
+ if not email_addr or not password:
300
+ return "❌ Email and password are required", "", []
301
+
302
+ # Get server configuration
303
+ if provider == "Custom":
304
+ if not custom_server:
305
+ return "❌ Custom server address is required", "", []
306
+ server_addr = custom_server
307
+ port = custom_port
308
+ else:
309
+ if provider not in EMAIL_SERVERS:
310
+ return f"❌ Unsupported provider: {provider}", "", []
311
+
312
+ server_config = EMAIL_SERVERS[provider][protocol.lower()]
313
+ server_addr = server_config["server"]
314
+ port = server_config["port"]
315
+
316
+ # Connect to email server
317
+ if protocol.lower() == "imap":
318
+ success, message = email_client.connect_imap(server_addr, port, email_addr, password)
319
+ else:
320
+ success, message = email_client.connect_pop3(server_addr, port, email_addr, password)
321
+
322
+ if success:
323
+ # Get folders if IMAP
324
+ if protocol.lower() == "imap":
325
+ folders, _ = email_client.get_folders()
326
+ return message, f"Connected as: {email_addr}", gr.update(choices=folders, value="INBOX")
327
+ else:
328
+ return message, f"Connected as: {email_addr}", gr.update(choices=["INBOX"], value="INBOX")
329
+ else:
330
+ return message, "", []
331
+
332
+ def fetch_emails_ui(folder, limit, search_criteria):
333
+ """Fetch emails and return as DataFrame"""
334
+ if not email_client.protocol:
335
+ return None, "❌ Not connected to email server"
336
+
337
+ emails, message = email_client.fetch_emails(folder, limit, search_criteria)
338
+
339
+ if not emails:
340
+ return None, message
341
+
342
+ # Convert to DataFrame for better display
343
+ df_data = []
344
+ for email_data in emails:
345
+ df_data.append({
346
+ 'Subject': email_data['subject'][:50] + "..." if len(email_data['subject']) > 50 else email_data['subject'],
347
+ 'From': email_data['from'],
348
+ 'Date': email_data['date'],
349
+ 'Attachments': email_data['attachment_count'],
350
+ 'Preview': (email_data['body_text'][:100] + "...") if email_data['body_text'] else "No text content"
351
+ })
352
+
353
+ df = pd.DataFrame(df_data)
354
+ return df, message
355
+
356
+ def search_emails_ui(query, folder, limit):
357
+ """Search emails with query"""
358
+ if not email_client.protocol:
359
+ return None, "❌ Not connected to email server"
360
+
361
+ if not query.strip():
362
+ return None, "❌ Search query is required"
363
+
364
+ # Create search criteria
365
+ if email_client.protocol == "IMAP":
366
+ search_criteria = f'(OR (SUBJECT "{query}") (FROM "{query}") (BODY "{query}"))'
367
+ else:
368
+ search_criteria = "ALL" # POP3 doesn't support server-side search
369
+
370
+ emails, message = email_client.fetch_emails(folder, limit, search_criteria)
371
+
372
+ if not emails:
373
+ return None, message
374
+
375
+ # Additional client-side filtering for POP3 or better results
376
+ filtered_emails = []
377
+ query_lower = query.lower()
378
+ for email_data in emails:
379
+ if (query_lower in email_data.get('subject', '').lower() or
380
+ query_lower in email_data.get('from', '').lower() or
381
+ query_lower in email_data.get('body_text', '').lower()):
382
+ filtered_emails.append(email_data)
383
+
384
+ if not filtered_emails:
385
+ return None, f"❌ No emails found matching query: {query}"
386
+
387
+ # Convert to DataFrame
388
+ df_data = []
389
+ for email_data in filtered_emails:
390
+ df_data.append({
391
+ 'Subject': email_data['subject'][:50] + "..." if len(email_data['subject']) > 50 else email_data['subject'],
392
+ 'From': email_data['from'],
393
+ 'Date': email_data['date'],
394
+ 'Attachments': email_data['attachment_count'],
395
+ 'Preview': (email_data['body_text'][:100] + "...") if email_data['body_text'] else "No text content"
396
+ })
397
+
398
+ df = pd.DataFrame(df_data)
399
+ return df, f"βœ… Found {len(filtered_emails)} emails matching '{query}'"
400
+
401
+ def disconnect_email():
402
+ """Disconnect from email server"""
403
+ message = email_client.disconnect()
404
+ return message, "", []
405
+
406
+ def get_connection_status():
407
+ """Get current connection status"""
408
+ if email_client.protocol and email_client.connected_email:
409
+ return f"🟒 Connected to {email_client.connected_email} via {email_client.protocol}"
410
+ else:
411
+ return "πŸ”΄ Not connected"
412
+
413
+ def update_custom_server_visibility(provider):
414
+ """Show/hide custom server fields based on provider selection"""
415
+ if provider == "Custom":
416
+ return gr.update(visible=True), gr.update(visible=True)
417
+ else:
418
+ return gr.update(visible=False), gr.update(visible=False)
419
+
420
+ # Create Gradio interface
421
+ with gr.Blocks(title="Email Client", theme=gr.themes.Soft()) as app:
422
+ gr.Markdown("# πŸ“§ Email Client")
423
+ gr.Markdown("Connect to your email account and manage your emails with a user-friendly interface.")
424
+
425
+ # Connection status
426
+ with gr.Row():
427
+ status_display = gr.Textbox(
428
+ label="Connection Status",
429
+ value=get_connection_status(),
430
+ interactive=False,
431
+ scale=4
432
+ )
433
+ refresh_status_btn = gr.Button("πŸ”„ Refresh Status", scale=1)
434
+
435
+ with gr.Tabs():
436
+ # Connection Tab
437
+ with gr.TabItem("πŸ”— Connect"):
438
+ gr.Markdown("### Email Connection Settings")
439
+
440
+ with gr.Row():
441
+ email_input = gr.Textbox(
442
+ label="Email Address",
443
+ placeholder="[email protected]",
444
+ scale=3
445
+ )
446
+ password_input = gr.Textbox(
447
+ label="Password",
448
+ placeholder="Your password or app password",
449
+ type="password",
450
+ scale=3
451
+ )
452
+
453
+ with gr.Row():
454
+ provider_dropdown = gr.Dropdown(
455
+ choices=list(EMAIL_SERVERS.keys()) + ["Custom"],
456
+ label="Email Provider",
457
+ value="Gmail",
458
+ scale=2
459
+ )
460
+ protocol_dropdown = gr.Dropdown(
461
+ choices=["IMAP", "POP3"],
462
+ label="Protocol",
463
+ value="IMAP",
464
+ scale=1
465
+ )
466
+
467
+ # Custom server fields (initially hidden)
468
+ with gr.Row():
469
+ custom_server_input = gr.Textbox(
470
+ label="Custom Server",
471
+ placeholder="mail.example.com",
472
+ visible=False,
473
+ scale=3
474
+ )
475
+ custom_port_input = gr.Number(
476
+ label="Port",
477
+ value=993,
478
+ visible=False,
479
+ scale=1
480
+ )
481
+
482
+ with gr.Row():
483
+ connect_btn = gr.Button("πŸ”Œ Connect", variant="primary", scale=1)
484
+ disconnect_btn = gr.Button("❌ Disconnect", scale=1)
485
+
486
+ connection_message = gr.Textbox(
487
+ label="Connection Message",
488
+ interactive=False
489
+ )
490
+
491
+ gr.Markdown("""
492
+ ### πŸ’‘ Connection Tips:
493
+ - **Gmail**: Use app passwords instead of your regular password
494
+ - **IMAP**: Recommended for full folder access and search capabilities
495
+ - **POP3**: Downloads emails to local client, limited folder support
496
+ """)
497
+
498
+ # Email Management Tab
499
+ with gr.TabItem("πŸ“¬ Emails"):
500
+ with gr.Row():
501
+ folder_dropdown = gr.Dropdown(
502
+ label="Folder",
503
+ choices=["INBOX"],
504
+ value="INBOX",
505
+ scale=2
506
+ )
507
+ limit_slider = gr.Slider(
508
+ minimum=1,
509
+ maximum=100,
510
+ value=10,
511
+ step=1,
512
+ label="Email Limit",
513
+ scale=1
514
+ )
515
+
516
+ with gr.Row():
517
+ search_criteria_input = gr.Textbox(
518
+ label="Search Criteria (IMAP only)",
519
+ placeholder="ALL, UNSEEN, FROM [email protected]",
520
+ value="ALL",
521
+ scale=3
522
+ )
523
+ fetch_btn = gr.Button("πŸ“₯ Fetch Emails", variant="primary", scale=1)
524
+
525
+ fetch_message = gr.Textbox(
526
+ label="Fetch Status",
527
+ interactive=False
528
+ )
529
+
530
+ emails_dataframe = gr.Dataframe(
531
+ label="Emails",
532
+ headers=["Subject", "From", "Date", "Attachments", "Preview"],
533
+ interactive=False
534
+ )
535
+
536
+ # Search Tab
537
+ with gr.TabItem("πŸ” Search"):
538
+ with gr.Row():
539
+ search_query_input = gr.Textbox(
540
+ label="Search Query",
541
+ placeholder="Enter keywords to search in subject, sender, or body",
542
+ scale=3
543
+ )
544
+ search_btn = gr.Button("πŸ” Search", variant="primary", scale=1)
545
+
546
+ with gr.Row():
547
+ search_folder_dropdown = gr.Dropdown(
548
+ label="Search in Folder",
549
+ choices=["INBOX"],
550
+ value="INBOX",
551
+ scale=2
552
+ )
553
+ search_limit_slider = gr.Slider(
554
+ minimum=1,
555
+ maximum=100,
556
+ value=20,
557
+ step=1,
558
+ label="Max Results",
559
+ scale=1
560
+ )
561
+
562
+ search_message = gr.Textbox(
563
+ label="Search Status",
564
+ interactive=False
565
+ )
566
+
567
+ search_results_dataframe = gr.Dataframe(
568
+ label="Search Results",
569
+ headers=["Subject", "From", "Date", "Attachments", "Preview"],
570
+ interactive=False
571
+ )
572
+
573
+ # Help Tab
574
+ with gr.TabItem("❓ Help"):
575
+ gr.Markdown("""
576
+ ## How to Use This Email Client
577
+
578
+ ### 1. **Connect to Your Email**
579
+ - Enter your email address and password
580
+ - For **Gmail**: You need to use an "App Password" instead of your regular password
581
+ - Go to your Google Account settings
582
+ - Enable 2-Step Verification
583
+ - Generate an App Password for "Mail"
584
+ - Choose your email provider or use "Custom" for other providers
585
+ - Select IMAP (recommended) or POP3 protocol
586
+
587
+ ### 2. **Fetch Emails**
588
+ - Select a folder (INBOX is default)
589
+ - Set the number of emails to fetch
590
+ - Use search criteria for IMAP (e.g., "UNSEEN" for unread emails)
591
+
592
+ ### 3. **Search Emails**
593
+ - Enter keywords to search in subject, sender, or email body
594
+ - Choose the folder to search in
595
+ - Set maximum number of results to return
596
+
597
+ ### **Supported Email Providers:**
598
+ - Gmail (imap.gmail.com, pop.gmail.com)
599
+ - Outlook/Hotmail (outlook.office365.com)
600
+ - Yahoo (imap.mail.yahoo.com, pop.mail.yahoo.com)
601
+ - iCloud (imap.mail.me.com, pop.mail.me.com)
602
+ - Custom servers
603
+
604
+ ### **Security Notes:**
605
+ - Your credentials are only used for the current session
606
+ - Use app-specific passwords when available
607
+ - All connections use SSL/TLS encryption
608
+ """)
609
+
610
+ # Event handlers
611
+ provider_dropdown.change(
612
+ fn=update_custom_server_visibility,
613
+ inputs=[provider_dropdown],
614
+ outputs=[custom_server_input, custom_port_input]
615
+ )
616
+
617
+ connect_btn.click(
618
+ fn=connect_to_email,
619
+ inputs=[email_input, password_input, provider_dropdown, protocol_dropdown, custom_server_input, custom_port_input],
620
+ outputs=[connection_message, status_display, folder_dropdown]
621
+ )
622
+
623
+ disconnect_btn.click(
624
+ fn=disconnect_email,
625
+ outputs=[connection_message, status_display, folder_dropdown]
626
+ )
627
+
628
+ refresh_status_btn.click(
629
+ fn=get_connection_status,
630
+ outputs=[status_display]
631
+ )
632
+
633
+ fetch_btn.click(
634
+ fn=fetch_emails_ui,
635
+ inputs=[folder_dropdown, limit_slider, search_criteria_input],
636
+ outputs=[emails_dataframe, fetch_message]
637
+ )
638
+
639
+ search_btn.click(
640
+ fn=search_emails_ui,
641
+ inputs=[search_query_input, search_folder_dropdown, search_limit_slider],
642
+ outputs=[search_results_dataframe, search_message]
643
+ )
644
+
645
+ # Update search folder dropdown when main folder dropdown changes
646
+ folder_dropdown.change(
647
+ fn=lambda x: gr.update(value=x),
648
+ inputs=[folder_dropdown],
649
+ outputs=[search_folder_dropdown]
650
+ )
651
+
652
+ # Launch the app
653
+ if __name__ == "__main__":
654
+ app.launch(
655
+ server_name="0.0.0.0",
656
+ server_port=7860,
657
+ share=True,
658
+ show_error=True
659
+ )