medicafe 0.250728.9__py3-none-any.whl → 0.250805.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

Files changed (57) hide show
  1. MediBot/MediBot.bat +233 -19
  2. MediBot/MediBot.py +138 -46
  3. MediBot/MediBot_Crosswalk_Library.py +127 -623
  4. MediBot/MediBot_Crosswalk_Utils.py +618 -0
  5. MediBot/MediBot_Preprocessor.py +72 -17
  6. MediBot/MediBot_Preprocessor_lib.py +470 -76
  7. MediBot/MediBot_UI.py +32 -17
  8. MediBot/MediBot_dataformat_library.py +68 -20
  9. MediBot/MediBot_docx_decoder.py +120 -19
  10. MediBot/MediBot_smart_import.py +180 -0
  11. MediBot/__init__.py +89 -0
  12. MediBot/get_medicafe_version.py +25 -0
  13. MediBot/update_json.py +35 -6
  14. MediBot/update_medicafe.py +19 -1
  15. MediCafe/MediLink_ConfigLoader.py +160 -0
  16. MediCafe/__init__.py +171 -0
  17. MediCafe/__main__.py +314 -0
  18. MediCafe/api_core.py +1098 -0
  19. MediCafe/api_core_backup.py +427 -0
  20. MediCafe/api_factory.py +306 -0
  21. MediCafe/api_utils.py +356 -0
  22. MediCafe/core_utils.py +450 -0
  23. MediCafe/graphql_utils.py +445 -0
  24. MediCafe/logging_config.py +123 -0
  25. MediCafe/logging_demo.py +61 -0
  26. MediCafe/migration_helpers.py +463 -0
  27. MediCafe/smart_import.py +436 -0
  28. MediLink/MediLink_837p_cob_library.py +28 -28
  29. MediLink/MediLink_837p_encoder.py +33 -34
  30. MediLink/MediLink_837p_encoder_library.py +226 -150
  31. MediLink/MediLink_837p_utilities.py +129 -5
  32. MediLink/MediLink_API_Generator.py +83 -60
  33. MediLink/MediLink_API_v3.py +1 -1
  34. MediLink/MediLink_ClaimStatus.py +177 -31
  35. MediLink/MediLink_DataMgmt.py +378 -63
  36. MediLink/MediLink_Decoder.py +20 -1
  37. MediLink/MediLink_Deductible.py +155 -28
  38. MediLink/MediLink_Display_Utils.py +72 -0
  39. MediLink/MediLink_Down.py +127 -5
  40. MediLink/MediLink_Gmail.py +720 -653
  41. MediLink/MediLink_PatientProcessor.py +257 -0
  42. MediLink/MediLink_UI.py +85 -71
  43. MediLink/MediLink_Up.py +28 -4
  44. MediLink/MediLink_insurance_utils.py +227 -230
  45. MediLink/MediLink_main.py +248 -0
  46. MediLink/MediLink_smart_import.py +264 -0
  47. MediLink/__init__.py +93 -1
  48. MediLink/insurance_type_integration_test.py +13 -3
  49. MediLink/test.py +1 -1
  50. MediLink/test_timing.py +59 -0
  51. {medicafe-0.250728.9.dist-info → medicafe-0.250805.2.dist-info}/METADATA +1 -1
  52. medicafe-0.250805.2.dist-info/RECORD +81 -0
  53. medicafe-0.250805.2.dist-info/entry_points.txt +2 -0
  54. {medicafe-0.250728.9.dist-info → medicafe-0.250805.2.dist-info}/top_level.txt +1 -0
  55. medicafe-0.250728.9.dist-info/RECORD +0 -59
  56. {medicafe-0.250728.9.dist-info → medicafe-0.250805.2.dist-info}/LICENSE +0 -0
  57. {medicafe-0.250728.9.dist-info → medicafe-0.250805.2.dist-info}/WHEEL +0 -0
@@ -1,654 +1,721 @@
1
- # MediLink_Gmail.py
2
- import sys, os, subprocess, time, webbrowser, requests, json, ssl, signal
3
- from MediLink_ConfigLoader import log, load_configuration
4
- from http.server import BaseHTTPRequestHandler, HTTPServer
5
- from threading import Thread, Event
6
- import platform
7
- import ctypes
8
-
9
- config, _ = load_configuration()
10
- local_storage_path = config['MediLink_Config']['local_storage_path']
11
- downloaded_emails_file = os.path.join(local_storage_path, 'downloaded_emails.txt')
12
-
13
- server_port = 8000
14
- cert_file = 'server.cert'
15
- key_file = 'server.key'
16
- # Try to find openssl.cnf in various locations
17
- openssl_cnf = 'MediLink\\openssl.cnf'
18
- if not os.path.exists(openssl_cnf):
19
- log("Could not find openssl.cnf at: " + os.path.abspath(openssl_cnf))
20
- # Try one directory up
21
- parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
- alternative_path = os.path.join(parent_dir, 'MediBot', 'openssl.cnf')
23
- log("Trying alternative path: " + alternative_path)
24
- if os.path.exists(alternative_path):
25
- openssl_cnf = alternative_path
26
- log("Found openssl.cnf at: " + openssl_cnf)
27
- else:
28
- log("Could not find openssl.cnf at alternative path either")
29
-
30
- httpd = None # Global variable for the HTTP server
31
- shutdown_event = Event() # Event to signal shutdown
32
-
33
- # Define the scopes for the Gmail API and other required APIs
34
- SCOPES = ' '.join([
35
- 'https://www.googleapis.com/auth/gmail.modify',
36
- 'https://www.googleapis.com/auth/gmail.compose',
37
- 'https://www.googleapis.com/auth/gmail.readonly',
38
- 'https://www.googleapis.com/auth/script.external_request',
39
- 'https://www.googleapis.com/auth/userinfo.email',
40
- 'https://www.googleapis.com/auth/script.scriptapp',
41
- 'https://www.googleapis.com/auth/drive'
42
- ])
43
-
44
- # Path to token.json file
45
- TOKEN_PATH = 'token.json'
46
-
47
- # Determine the operating system and version
48
- os_name = platform.system()
49
- os_version = platform.release()
50
-
51
- # Set the credentials path based on the OS and version
52
- if os_name == 'Windows' and 'XP' in os_version:
53
- CREDENTIALS_PATH = 'F:\\Medibot\\json\\credentials.json'
54
- else:
55
- CREDENTIALS_PATH = 'json\\credentials.json'
56
-
57
- # Log the selected path for verification
58
- log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
59
-
60
- REDIRECT_URI = 'https://127.0.0.1:8000'
61
-
62
- def get_authorization_url():
63
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
64
- credentials = json.load(credentials_file)
65
- client_id = credentials['web']['client_id']
66
- auth_url = (
67
- "https://accounts.google.com/o/oauth2/v2/auth?"
68
- "response_type=code&"
69
- "client_id={}&"
70
- "redirect_uri={}&"
71
- "scope={}&"
72
- "access_type=offline&" # Requesting offline access allows the application to obtain a refresh token, enabling it to access resources even when the user is not actively using the app. This is useful for long-lived sessions.
73
- # To improve user experience, consider changing this to 'online' if you don't need offline access:
74
- # "access_type=online&" # Use this if you only need access while the user is actively using the app and don't require a refresh token.
75
-
76
- "prompt=consent" # This forces the user to re-consent to the requested scopes every time they authenticate. While this is useful for ensuring the user is aware of the permissions being granted, it can be modified to 'none' or omitted entirely if the application is functioning correctly and tokens are being refreshed properly.
77
- # To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
78
- # "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
79
- # Alternatively, you can omit the prompt parameter entirely to use the default behavior:
80
- # # "prompt=" # Omitting this will show the consent screen only when necessary.
81
- ).format(client_id, REDIRECT_URI, SCOPES)
82
- log("Generated authorization URL: {}".format(auth_url))
83
- return auth_url
84
-
85
- def exchange_code_for_token(auth_code, retries=3):
86
- for attempt in range(retries):
87
- try:
88
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
89
- credentials = json.load(credentials_file)
90
- token_url = "https://oauth2.googleapis.com/token"
91
- data = {
92
- 'code': auth_code,
93
- 'client_id': credentials['web']['client_id'],
94
- 'client_secret': credentials['web']['client_secret'],
95
- 'redirect_uri': REDIRECT_URI,
96
- 'grant_type': 'authorization_code'
97
- }
98
- response = requests.post(token_url, data=data)
99
- log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
100
- token_response = response.json()
101
- if response.status_code == 200:
102
- token_response['token_time'] = time.time()
103
- return token_response
104
- else:
105
- log("Token exchange failed: {}".format(token_response))
106
- if attempt < retries - 1:
107
- log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
108
- except Exception as e:
109
- log("Error during token exchange: {}".format(e))
110
- return {}
111
-
112
- def get_access_token():
113
- if os.path.exists(TOKEN_PATH):
114
- with open(TOKEN_PATH, 'r') as token_file:
115
- token_data = json.load(token_file)
116
- log("Loaded token data:\n {}".format(token_data))
117
-
118
- if 'access_token' in token_data and 'expires_in' in token_data:
119
- try:
120
- # Use current time if 'token_time' is missing
121
- token_time = token_data.get('token_time', time.time())
122
- token_expiry_time = token_time + token_data['expires_in']
123
-
124
- except KeyError as e:
125
- log("KeyError while accessing token data: {}".format(e))
126
- return None
127
-
128
- if token_expiry_time > time.time():
129
- log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
130
- return token_data['access_token']
131
- else:
132
- log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
133
- new_token_data = refresh_access_token(token_data.get('refresh_token'))
134
- if 'access_token' in new_token_data:
135
- new_token_data['token_time'] = time.time()
136
- with open(TOKEN_PATH, 'w') as token_file:
137
- json.dump(new_token_data, token_file)
138
- log("Access token refreshed successfully. New token data: {}".format(new_token_data))
139
- return new_token_data['access_token']
140
- else:
141
- log("Failed to refresh access token. New token data: {}".format(new_token_data))
142
- return None
143
- log("Access token not found. Please authenticate.")
144
- return None
145
-
146
- def refresh_access_token(refresh_token):
147
- log("Refreshing access token.")
148
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
149
- credentials = json.load(credentials_file)
150
- token_url = "https://oauth2.googleapis.com/token"
151
- data = {
152
- 'client_id': credentials['web']['client_id'],
153
- 'client_secret': credentials['web']['client_secret'],
154
- 'refresh_token': refresh_token,
155
- 'grant_type': 'refresh_token'
156
- }
157
- response = requests.post(token_url, data=data)
158
- log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
159
- if response.status_code == 200:
160
- log("Access token refreshed successfully.")
161
- return response.json()
162
- else:
163
- log("Failed to refresh access token. Status code: {}".format(response.status_code))
164
- return {}
165
-
166
- def bring_window_to_foreground():
167
- """Brings the current window to the foreground on Windows."""
168
- try:
169
- if platform.system() == 'Windows':
170
- # Get the current process ID
171
- pid = os.getpid()
172
- # Get the window handle for the current process
173
- hwnd = ctypes.windll.user32.GetForegroundWindow()
174
- # Get the process ID of the window
175
- current_pid = ctypes.c_ulong()
176
- ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(current_pid))
177
-
178
- # If the window is not ours, try to bring it to front
179
- if current_pid.value != pid:
180
- # Try to set the window to foreground
181
- ctypes.windll.user32.SetForegroundWindow(hwnd)
182
- # If that fails, try the alternative method
183
- if ctypes.windll.user32.GetForegroundWindow() != hwnd:
184
- ctypes.windll.user32.ShowWindow(hwnd, 9) # SW_RESTORE = 9
185
- ctypes.windll.user32.SetForegroundWindow(hwnd)
186
- except Exception as e:
187
- log("Error bringing window to foreground: {}".format(e))
188
-
189
- class RequestHandler(BaseHTTPRequestHandler):
190
- def _set_headers(self):
191
- self.send_header('Access-Control-Allow-Origin', '*')
192
- self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
193
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
194
- self.send_header('Content-type', 'application/json')
195
-
196
- def do_OPTIONS(self):
197
- self.send_response(200)
198
- self._set_headers()
199
- self.end_headers()
200
-
201
- def do_POST(self):
202
- if self.path == '/download':
203
- content_length = int(self.headers['Content-Length'])
204
- post_data = self.rfile.read(content_length)
205
- data = json.loads(post_data.decode('utf-8'))
206
- links = data.get('links', [])
207
-
208
- # Log the content of links
209
- log("Received links: {}".format(links))
210
-
211
- file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
212
- log("File IDs received from client: {}".format(file_ids))
213
-
214
- # Proceed with downloading files
215
- download_docx_files(links)
216
- self.send_response(200)
217
- self._set_headers() # Include CORS headers
218
- self.end_headers()
219
- response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
220
- self.wfile.write(response.encode('utf-8'))
221
- shutdown_event.set()
222
- bring_window_to_foreground() # Bring window to foreground after download
223
- elif self.path == '/shutdown':
224
- log("Shutdown request received.")
225
- self.send_response(200)
226
- self._set_headers()
227
- self.end_headers()
228
- response = json.dumps({"status": "success", "message": "Server is shutting down."})
229
- self.wfile.write(response.encode('utf-8'))
230
- shutdown_event.set() # Signal shutdown event instead of calling stop_server directly
231
- elif self.path == '/delete-files':
232
- content_length = int(self.headers['Content-Length'])
233
- post_data = self.rfile.read(content_length)
234
- data = json.loads(post_data.decode('utf-8'))
235
- file_ids = data.get('fileIds', [])
236
- log("File IDs to delete received from client: {}".format(file_ids))
237
-
238
- if not isinstance(file_ids, list):
239
- self.send_response(400)
240
- self._set_headers()
241
- self.end_headers()
242
- response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
243
- self.wfile.write(response.encode('utf-8'))
244
- return
245
-
246
- self.send_response(200)
247
- self._set_headers() # Include CORS headers
248
- self.end_headers()
249
- response = json.dumps({"status": "success", "message": "Files deleted successfully."})
250
- self.wfile.write(response.encode('utf-8'))
251
- else:
252
- self.send_response(404)
253
- self.end_headers()
254
-
255
- def do_GET(self):
256
- log("Full request path: {}".format(self.path)) # Log the full path for debugging
257
- if self.path.startswith("/?code="):
258
- auth_code = self.path.split('=')[1].split('&')[0]
259
- auth_code = requests.utils.unquote(auth_code) # Decode if URL-encoded
260
- log("Received authorization code: {}".format(auth_code))
261
- if is_valid_authorization_code(auth_code):
262
- try:
263
- token_response = exchange_code_for_token(auth_code)
264
- if 'access_token' not in token_response:
265
- # Check for specific error message
266
- if token_response.get("status") == "error":
267
- self.send_response(400)
268
- self.send_header('Content-type', 'text/html')
269
- self.end_headers()
270
- self.wfile.write(token_response["message"].encode())
271
- return
272
- # Handle other cases
273
- raise ValueError("Access token not found in response.")
274
- except Exception as e:
275
- log("Error during token exchange: {}".format(e))
276
- self.send_response(500)
277
- self.send_header('Content-type', 'text/html')
278
- self.end_headers()
279
- self.wfile.write("An error occurred during authentication. Please try again.".encode())
280
- else:
281
- log("Token response: {}".format(token_response)) # Add this line
282
- if 'access_token' in token_response:
283
- with open(TOKEN_PATH, 'w') as token_file:
284
- json.dump(token_response, token_file)
285
- self.send_response(200)
286
- self.send_header('Content-type', 'text/html')
287
- self.end_headers()
288
- self.wfile.write("Authentication successful. You can close this window now.".encode())
289
- initiate_link_retrieval(config) # Pass config here
290
- else:
291
- log("Authentication failed with response: {}".format(token_response)) # Log the full response
292
- if 'error' in token_response:
293
- error_description = token_response.get('error_description', 'No description provided.')
294
- log("Error details: {}".format(error_description)) # Log specific error details
295
-
296
- # Provide user feedback based on the error
297
- if token_response.get('error') == 'invalid_grant':
298
- log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
299
- check_invalid_grant_causes(auth_code)
300
- clear_token_cache() # Clear the cache on invalid grant
301
- user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
302
- else:
303
- user_message = "Authentication failed. Please check the logs for more details."
304
-
305
- self.send_response(400)
306
- self.send_header('Content-type', 'text/html')
307
- self.end_headers()
308
- self.wfile.write(user_message.encode())
309
- shutdown_event.set() # Signal shutdown event after failed authentication
310
- else:
311
- log("Invalid authorization code format: {}".format(auth_code))
312
- self.send_response(400)
313
- self.send_header('Content-type', 'text/html')
314
- self.end_headers()
315
- self.wfile.write("Invalid authorization code format. Please try again.".encode())
316
- shutdown_event.set() # Signal shutdown event after failed authentication
317
- elif self.path == '/downloaded-emails':
318
- self.send_response(200)
319
- self._set_headers()
320
- self.end_headers()
321
- downloaded_emails = load_downloaded_emails()
322
- response = json.dumps({"downloadedEmails": list(downloaded_emails)})
323
- self.wfile.write(response.encode('utf-8'))
324
- else:
325
- self.send_response(200)
326
- self.send_header('Access-Control-Allow-Origin', '*')
327
- self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
328
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
329
- self.send_header('Content-type', 'text/html')
330
- self.end_headers()
331
- self.wfile.write(b'HTTPS server is running.')
332
-
333
- def generate_self_signed_cert(cert_file, key_file):
334
- log("Checking if certificate file exists: " + cert_file)
335
- log("Checking if key file exists: " + key_file)
336
-
337
- # Check if certificate exists and is not expired
338
- cert_needs_regeneration = True
339
- if os.path.exists(cert_file):
340
- try:
341
- # Check certificate expiration
342
- check_cmd = ['openssl', 'x509', '-in', cert_file, '-checkend', '86400', '-noout'] # Check if expires in next 24 hours
343
- result = subprocess.call(check_cmd)
344
- if result == 0:
345
- log("Certificate is still valid")
346
- cert_needs_regeneration = False
347
- else:
348
- log("Certificate is expired or will expire soon")
349
- # Delete expired certificate and key files
350
- try:
351
- if os.path.exists(cert_file):
352
- os.remove(cert_file)
353
- log("Deleted expired certificate file: {}".format(cert_file))
354
- if os.path.exists(key_file):
355
- os.remove(key_file)
356
- log("Deleted expired key file: {}".format(key_file))
357
- except Exception as e:
358
- log("Error deleting expired certificate files: {}".format(e))
359
- except Exception as e:
360
- log("Error checking certificate expiration: {}".format(e))
361
-
362
- if cert_needs_regeneration:
363
- log("Generating self-signed SSL certificate...")
364
- cmd = [
365
- 'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
366
- '-keyout', key_file,
367
- '-out', cert_file,
368
- '-days', '365',
369
- '-sha256' # Use SHA-256 for better security
370
- #'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
371
- ]
372
- try:
373
- log("Running command: " + ' '.join(cmd))
374
- result = subprocess.call(cmd)
375
- log("Command finished with result: " + str(result))
376
- if result != 0:
377
- raise RuntimeError("Failed to generate self-signed certificate")
378
-
379
- # Verify the certificate was generated correctly
380
- verify_cmd = ['openssl', 'x509', '-in', cert_file, '-text', '-noout']
381
- verify_result = subprocess.call(verify_cmd)
382
- if verify_result != 0:
383
- raise RuntimeError("Generated certificate verification failed")
384
-
385
- log("Self-signed SSL certificate generated and verified successfully.")
386
- except Exception as e:
387
- log("Error generating self-signed certificate: {}".format(e))
388
- raise
389
-
390
- def run_server():
391
- global httpd
392
- try:
393
- log("Attempting to start server on port " + str(server_port))
394
- server_address = ('0.0.0.0', server_port) # Bind to all interfaces
395
- httpd = HTTPServer(server_address, RequestHandler)
396
- log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
397
-
398
- if not os.path.exists(cert_file):
399
- log("Error: Certificate file not found: " + cert_file)
400
- if not os.path.exists(key_file):
401
- log("Error: Key file not found: " + key_file)
402
-
403
- httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
404
- log("Starting HTTPS server on port {}".format(server_port))
405
- httpd.serve_forever()
406
- except Exception as e:
407
- log("Error in serving: {}".format(e))
408
- stop_server()
409
-
410
- def stop_server():
411
- global httpd
412
- if httpd:
413
- log("Stopping HTTPS server.")
414
- httpd.shutdown()
415
- httpd.server_close()
416
- log("HTTPS server stopped.")
417
- shutdown_event.set() # Signal shutdown event
418
- bring_window_to_foreground() # Bring window to foreground after shutdown
419
-
420
- def load_downloaded_emails():
421
- downloaded_emails = set()
422
- if os.path.exists(downloaded_emails_file):
423
- with open(downloaded_emails_file, 'r') as file:
424
- downloaded_emails = set(line.strip() for line in file)
425
- log("Loaded downloaded emails: {}".format(downloaded_emails))
426
- return downloaded_emails
427
-
428
- def download_docx_files(links):
429
- # Load the set of downloaded emails
430
- # TODO Test if any of these have a .csv extension and then move those to the right location locally.
431
- downloaded_emails = load_downloaded_emails()
432
-
433
- for link in links:
434
- try:
435
- url = link.get('url', '')
436
- filename = link.get('filename', '')
437
-
438
- # Log the variables to debug
439
- log("Processing link: url='{}', filename='{}'".format(url, filename))
440
-
441
- # Skip if email already downloaded
442
- if filename in downloaded_emails:
443
- log("Skipping already downloaded email: {}".format(filename))
444
- continue
445
-
446
- log("Downloading .docx file from URL: {}".format(url))
447
- response = requests.get(url, verify=False) # Set verify to False for self-signed certs
448
- if response.status_code == 200:
449
- file_path = os.path.join(local_storage_path, filename)
450
- with open(file_path, 'wb') as file:
451
- file.write(response.content)
452
- log("Downloaded .docx file: {}".format(filename))
453
- # Add to the set and save the updated list
454
- downloaded_emails.add(filename)
455
- with open(downloaded_emails_file, 'a') as file:
456
- file.write(filename + '\n')
457
- else:
458
- log("Failed to download .docx file from URL: {}. Status code: {}".format(url, response.status_code))
459
- except Exception as e:
460
- log("Error downloading .docx file from URL: {}. Error: {}".format(url, e))
461
-
462
- def open_browser_with_executable(url, browser_path=None):
463
- try:
464
- if browser_path:
465
- log("Attempting to open URL with provided executable: {} {}".format(browser_path, url))
466
- process = subprocess.Popen([browser_path, url], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
467
- stdout, stderr = process.communicate()
468
- if process.returncode == 0:
469
- log("Browser opened with provided executable path using subprocess.Popen.")
470
- else:
471
- log("Browser failed to open using subprocess.Popen. Return code: {}. Stderr: {}".format(process.returncode, stderr))
472
- else:
473
- log("No browser path provided. Attempting to open URL with default browser: {}".format(url))
474
- webbrowser.open(url)
475
- log("Default browser opened.")
476
- except Exception as e:
477
- log("Failed to open browser: {}".format(e))
478
-
479
- def initiate_link_retrieval(config):
480
- log("Initiating browser via implicit GET.")
481
- url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
482
- open_browser_with_executable(url_get)
483
-
484
- log("Preparing POST call.")
485
- url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
486
- downloaded_emails = list(load_downloaded_emails())
487
- payload = {
488
- "downloadedEmails": downloaded_emails
489
- }
490
-
491
- access_token = get_access_token()
492
- if not access_token:
493
- log("Access token not found. Please authenticate first.")
494
- shutdown_event.set() # Signal shutdown event if token is not found
495
- return
496
-
497
- # Inspect the token to check its validity and permissions
498
- token_info = inspect_token(access_token)
499
- if token_info is None:
500
- log("Access token is invalid. Please re-authenticate.")
501
- shutdown_event.set() # Signal shutdown event if token is invalid
502
- return
503
-
504
- # Proceed with the rest of the function if the token is valid
505
- headers = {
506
- 'Authorization': 'Bearer {}'.format(access_token),
507
- 'Content-Type': 'application/json'
508
- }
509
-
510
- log("Request headers: {}".format(headers))
511
- log("Request payload: {}".format(payload))
512
-
513
- handle_post_response(url, payload, headers)
514
-
515
- def handle_post_response(url, payload, headers):
516
- try:
517
- response = requests.post(url, json=payload, headers=headers)
518
- log("Response status code: {}".format(response.status_code))
519
- log("Response body: {}".format(response.text))
520
-
521
- if response.status_code == 200:
522
- response_data = response.json()
523
- log("Parsed response data: {}".format(response_data)) # Log the parsed response data
524
- if response_data.get("status") == "error":
525
- log("Error message from server: {}".format(response_data.get("message")))
526
- print("Error: {}".format(response_data.get("message")))
527
- shutdown_event.set() # Signal shutdown event after error
528
- else:
529
- log("Link retrieval initiated successfully.")
530
- elif response.status_code == 401:
531
- log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
532
- # Inspect the token to log its details
533
- token_info = inspect_token(headers['Authorization'].split(' ')[1])
534
- log("Token details: {}".format(token_info))
535
- shutdown_event.set()
536
- elif response.status_code == 403:
537
- log("Forbidden access. Ensure that the OAuth client has the correct permissions. Response body: {}".format(response.text))
538
- shutdown_event.set()
539
- elif response.status_code == 404:
540
- log("Not Found. Verify the URL and ensure the Apps Script is deployed correctly. Response body: {}".format(response.text))
541
- shutdown_event.set()
542
- else:
543
- log("Failed to initiate link retrieval. Unexpected status code: {}. Response body: {}".format(response.status_code, response.text))
544
- shutdown_event.set()
545
- except requests.exceptions.RequestException as e:
546
- log("RequestException during link retrieval initiation: {}".format(e))
547
- shutdown_event.set()
548
- except Exception as e:
549
- log("Unexpected error during link retrieval initiation: {}".format(e))
550
- shutdown_event.set()
551
-
552
- def inspect_token(access_token):
553
- info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
554
- try:
555
- response = requests.get(info_url)
556
- log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
557
-
558
- if response.status_code == 200:
559
- return response.json()
560
- else:
561
- log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
562
- # Check for invalid token
563
- if response.status_code == 400 and "invalid_token" in response.text:
564
- log("Access token is invalid. Deleting token.json and stopping the server.")
565
- delete_token_file() # Delete the token.json file
566
- print("Access token is invalid. Please re-authenticate and restart the server.")
567
- stop_server() # Stop the server
568
- return None # Return None for invalid tokens
569
- return None # Return None for other invalid tokens
570
- except Exception as e:
571
- log("Exception during token inspection: {}".format(e))
572
- return None
573
-
574
- def delete_token_file():
575
- try:
576
- if os.path.exists(TOKEN_PATH):
577
- os.remove(TOKEN_PATH)
578
- log("Deleted token.json successfully.")
579
- else:
580
- log("token.json does not exist.")
581
- except Exception as e:
582
- log("Error deleting token.json: {}".format(e))
583
-
584
- def signal_handler(sig, frame):
585
- log("Signal received: {}. Initiating shutdown.".format(sig))
586
- stop_server()
587
- sys.exit(0)
588
-
589
- def auth_and_retrieval():
590
- access_token = get_access_token()
591
- if not access_token:
592
- log("Access token not found or expired. Please authenticate first.")
593
- #print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
594
- auth_url = get_authorization_url()
595
- #print(auth_url)
596
- open_browser_with_executable(auth_url)
597
- shutdown_event.wait() # Wait for the shutdown event to be set after authentication
598
- else:
599
- log("Access token found. Proceeding.")
600
- initiate_link_retrieval(config) # Pass config here
601
- shutdown_event.wait() # Wait for the shutdown event to be set
602
-
603
- def is_valid_authorization_code(auth_code):
604
- # Check if the authorization code is not None and is a non-empty string
605
- if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
606
- return True
607
- log("Invalid authorization code format: {}".format(auth_code))
608
- return False
609
-
610
- def clear_token_cache():
611
- if os.path.exists(TOKEN_PATH):
612
- os.remove(TOKEN_PATH)
613
- log("Cleared token cache.")
614
-
615
- def check_invalid_grant_causes(auth_code):
616
- # TODO Implement this function in the future to check for common causes of invalid_grant error
617
- # Log potential causes for invalid_grant
618
- log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
619
- # Example checks (you can expand this based on your needs)
620
- """
621
- if is_code_used(auth_code):
622
- log("Authorization code has already been used.")
623
- if not is_redirect_uri_correct():
624
- log("Redirect URI does not match the registered URI.")
625
- """
626
-
627
- if __name__ == "__main__":
628
- signal.signal(signal.SIGINT, signal_handler)
629
- signal.signal(signal.SIGTERM, signal_handler)
630
-
631
- try:
632
- # Generate SSL certificate if it doesn't exist
633
- generate_self_signed_cert(cert_file, key_file)
634
-
635
- from threading import Thread
636
- log("Starting server thread.")
637
- server_thread = Thread(target=run_server)
638
- server_thread.daemon = True
639
- server_thread.start()
640
-
641
- auth_and_retrieval()
642
-
643
- log("Stopping HTTPS server.")
644
- stop_server() # Ensure the server is stopped
645
- log("Waiting for server thread to finish.")
646
- server_thread.join() # Wait for the server thread to finish
647
- except KeyboardInterrupt:
648
- log("KeyboardInterrupt received, stopping server.")
649
- stop_server()
650
- sys.exit(0)
651
- except Exception as e:
652
- log("An error occurred: {}".format(e))
653
- stop_server()
1
+ # MediLink_Gmail.py
2
+ import sys, os, subprocess, time, webbrowser, requests, json, ssl, signal
3
+ from MediCafe.core_utils import get_shared_config_loader
4
+
5
+ # Get shared config loader
6
+ MediLink_ConfigLoader = get_shared_config_loader()
7
+ if MediLink_ConfigLoader:
8
+ load_configuration = MediLink_ConfigLoader.load_configuration
9
+ log = MediLink_ConfigLoader.log
10
+ else:
11
+ # Fallback functions if config loader is not available
12
+ def load_configuration():
13
+ return {}, {}
14
+ def log(message, level="INFO"):
15
+ print("[{}] {}".format(level, message))
16
+ from http.server import BaseHTTPRequestHandler, HTTPServer
17
+ from threading import Thread, Event
18
+ import platform
19
+ import ctypes
20
+
21
+ config, _ = load_configuration()
22
+ local_storage_path = config['MediLink_Config']['local_storage_path']
23
+ downloaded_emails_file = os.path.join(local_storage_path, 'downloaded_emails.txt')
24
+
25
+ server_port = 8000
26
+ cert_file = 'server.cert'
27
+ key_file = 'server.key'
28
+ # Try to find openssl.cnf in various locations
29
+ openssl_cnf = 'openssl.cnf' # Use relative path since we're running from MediLink directory
30
+ if not os.path.exists(openssl_cnf):
31
+ log("Could not find openssl.cnf at: " + os.path.abspath(openssl_cnf))
32
+ # Try MediLink directory
33
+ medilink_dir = os.path.dirname(os.path.abspath(__file__))
34
+ medilink_openssl = os.path.join(medilink_dir, 'openssl.cnf')
35
+ log("Trying MediLink directory: " + medilink_openssl)
36
+ if os.path.exists(medilink_openssl):
37
+ openssl_cnf = medilink_openssl
38
+ log("Found openssl.cnf at: " + openssl_cnf)
39
+ else:
40
+ # Try one directory up
41
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
+ alternative_path = os.path.join(parent_dir, 'MediBot', 'openssl.cnf')
43
+ log("Trying alternative path: " + alternative_path)
44
+ if os.path.exists(alternative_path):
45
+ openssl_cnf = alternative_path
46
+ log("Found openssl.cnf at: " + openssl_cnf)
47
+ else:
48
+ log("Could not find openssl.cnf at alternative path either")
49
+
50
+ httpd = None # Global variable for the HTTP server
51
+ shutdown_event = Event() # Event to signal shutdown
52
+
53
+ # Define the scopes for the Gmail API and other required APIs
54
+ SCOPES = ' '.join([
55
+ 'https://www.googleapis.com/auth/gmail.modify',
56
+ 'https://www.googleapis.com/auth/gmail.compose',
57
+ 'https://www.googleapis.com/auth/gmail.readonly',
58
+ 'https://www.googleapis.com/auth/script.external_request',
59
+ 'https://www.googleapis.com/auth/userinfo.email',
60
+ 'https://www.googleapis.com/auth/script.scriptapp',
61
+ 'https://www.googleapis.com/auth/drive'
62
+ ])
63
+
64
+ # Path to token.json file
65
+ TOKEN_PATH = 'token.json'
66
+
67
+ # Determine the operating system and version
68
+ os_name = platform.system()
69
+ os_version = platform.release()
70
+
71
+ # Set the credentials path based on the OS and version
72
+ if os_name == 'Windows' and 'XP' in os_version:
73
+ CREDENTIALS_PATH = 'F:\\Medibot\\json\\credentials.json'
74
+ else:
75
+ CREDENTIALS_PATH = 'json\\credentials.json'
76
+
77
+ # Log the selected path for verification
78
+ log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
79
+
80
+ REDIRECT_URI = 'https://127.0.0.1:8000'
81
+
82
+ def get_authorization_url():
83
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
84
+ credentials = json.load(credentials_file)
85
+ client_id = credentials['web']['client_id']
86
+ auth_url = (
87
+ "https://accounts.google.com/o/oauth2/v2/auth?"
88
+ "response_type=code&"
89
+ "client_id={}&"
90
+ "redirect_uri={}&"
91
+ "scope={}&"
92
+ "access_type=offline&" # Requesting offline access allows the application to obtain a refresh token, enabling it to access resources even when the user is not actively using the app. This is useful for long-lived sessions.
93
+ # To improve user experience, consider changing this to 'online' if you don't need offline access:
94
+ # "access_type=online&" # Use this if you only need access while the user is actively using the app and don't require a refresh token.
95
+
96
+ "prompt=consent" # This forces the user to re-consent to the requested scopes every time they authenticate. While this is useful for ensuring the user is aware of the permissions being granted, it can be modified to 'none' or omitted entirely if the application is functioning correctly and tokens are being refreshed properly.
97
+ # To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
98
+ # "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
99
+ # Alternatively, you can omit the prompt parameter entirely to use the default behavior:
100
+ # # "prompt=" # Omitting this will show the consent screen only when necessary.
101
+ ).format(client_id, REDIRECT_URI, SCOPES)
102
+ log("Generated authorization URL: {}".format(auth_url))
103
+ return auth_url
104
+
105
+ def exchange_code_for_token(auth_code, retries=3):
106
+ for attempt in range(retries):
107
+ try:
108
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
109
+ credentials = json.load(credentials_file)
110
+ token_url = "https://oauth2.googleapis.com/token"
111
+ data = {
112
+ 'code': auth_code,
113
+ 'client_id': credentials['web']['client_id'],
114
+ 'client_secret': credentials['web']['client_secret'],
115
+ 'redirect_uri': REDIRECT_URI,
116
+ 'grant_type': 'authorization_code'
117
+ }
118
+ response = requests.post(token_url, data=data)
119
+ log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
120
+ token_response = response.json()
121
+ if response.status_code == 200:
122
+ token_response['token_time'] = time.time()
123
+ return token_response
124
+ else:
125
+ log("Token exchange failed: {}".format(token_response))
126
+ if attempt < retries - 1:
127
+ log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
128
+ except Exception as e:
129
+ log("Error during token exchange: {}".format(e))
130
+ return {}
131
+
132
+ def get_access_token():
133
+ if os.path.exists(TOKEN_PATH):
134
+ with open(TOKEN_PATH, 'r') as token_file:
135
+ token_data = json.load(token_file)
136
+ log("Loaded token data:\n {}".format(token_data))
137
+
138
+ if 'access_token' in token_data and 'expires_in' in token_data:
139
+ try:
140
+ # Use current time if 'token_time' is missing
141
+ token_time = token_data.get('token_time', time.time())
142
+ token_expiry_time = token_time + token_data['expires_in']
143
+
144
+ except KeyError as e:
145
+ log("KeyError while accessing token data: {}".format(e))
146
+ return None
147
+
148
+ if token_expiry_time > time.time():
149
+ log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
150
+ return token_data['access_token']
151
+ else:
152
+ log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
153
+ new_token_data = refresh_access_token(token_data.get('refresh_token'))
154
+ if 'access_token' in new_token_data:
155
+ new_token_data['token_time'] = time.time()
156
+ with open(TOKEN_PATH, 'w') as token_file:
157
+ json.dump(new_token_data, token_file)
158
+ log("Access token refreshed successfully. New token data: {}".format(new_token_data))
159
+ return new_token_data['access_token']
160
+ else:
161
+ log("Failed to refresh access token. New token data: {}".format(new_token_data))
162
+ return None
163
+ log("Access token not found. Please authenticate.")
164
+ return None
165
+
166
+ def refresh_access_token(refresh_token):
167
+ log("Refreshing access token.")
168
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
169
+ credentials = json.load(credentials_file)
170
+ token_url = "https://oauth2.googleapis.com/token"
171
+ data = {
172
+ 'client_id': credentials['web']['client_id'],
173
+ 'client_secret': credentials['web']['client_secret'],
174
+ 'refresh_token': refresh_token,
175
+ 'grant_type': 'refresh_token'
176
+ }
177
+ response = requests.post(token_url, data=data)
178
+ log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
179
+ if response.status_code == 200:
180
+ log("Access token refreshed successfully.")
181
+ return response.json()
182
+ else:
183
+ log("Failed to refresh access token. Status code: {}".format(response.status_code))
184
+ return {}
185
+
186
+ def bring_window_to_foreground():
187
+ """Brings the current window to the foreground on Windows."""
188
+ try:
189
+ if platform.system() == 'Windows':
190
+ # Get the current process ID
191
+ pid = os.getpid()
192
+ # Get the window handle for the current process
193
+ hwnd = ctypes.windll.user32.GetForegroundWindow()
194
+ # Get the process ID of the window
195
+ current_pid = ctypes.c_ulong()
196
+ ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(current_pid))
197
+
198
+ # If the window is not ours, try to bring it to front
199
+ if current_pid.value != pid:
200
+ # Try to set the window to foreground
201
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
202
+ # If that fails, try the alternative method
203
+ if ctypes.windll.user32.GetForegroundWindow() != hwnd:
204
+ ctypes.windll.user32.ShowWindow(hwnd, 9) # SW_RESTORE = 9
205
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
206
+ except Exception as e:
207
+ log("Error bringing window to foreground: {}".format(e))
208
+
209
+ class RequestHandler(BaseHTTPRequestHandler):
210
+ def _set_headers(self):
211
+ self.send_header('Access-Control-Allow-Origin', '*')
212
+ self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
213
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
214
+ self.send_header('Content-type', 'application/json')
215
+
216
+ def do_OPTIONS(self):
217
+ self.send_response(200)
218
+ self._set_headers()
219
+ self.end_headers()
220
+
221
+ def do_POST(self):
222
+ if self.path == '/download':
223
+ content_length = int(self.headers['Content-Length'])
224
+ post_data = self.rfile.read(content_length)
225
+ data = json.loads(post_data.decode('utf-8'))
226
+ links = data.get('links', [])
227
+
228
+ # Log the content of links
229
+ log("Received links: {}".format(links))
230
+
231
+ file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
232
+ log("File IDs received from client: {}".format(file_ids))
233
+
234
+ # Proceed with downloading files
235
+ download_docx_files(links)
236
+ self.send_response(200)
237
+ self._set_headers() # Include CORS headers
238
+ self.end_headers()
239
+ response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
240
+ self.wfile.write(response.encode('utf-8'))
241
+ shutdown_event.set()
242
+ bring_window_to_foreground() # Bring window to foreground after download
243
+ elif self.path == '/shutdown':
244
+ log("Shutdown request received.")
245
+ self.send_response(200)
246
+ self._set_headers()
247
+ self.end_headers()
248
+ response = json.dumps({"status": "success", "message": "Server is shutting down."})
249
+ self.wfile.write(response.encode('utf-8'))
250
+ shutdown_event.set() # Signal shutdown event instead of calling stop_server directly
251
+ elif self.path == '/delete-files':
252
+ content_length = int(self.headers['Content-Length'])
253
+ post_data = self.rfile.read(content_length)
254
+ data = json.loads(post_data.decode('utf-8'))
255
+ file_ids = data.get('fileIds', [])
256
+ log("File IDs to delete received from client: {}".format(file_ids))
257
+
258
+ if not isinstance(file_ids, list):
259
+ self.send_response(400)
260
+ self._set_headers()
261
+ self.end_headers()
262
+ response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
263
+ self.wfile.write(response.encode('utf-8'))
264
+ return
265
+
266
+ self.send_response(200)
267
+ self._set_headers() # Include CORS headers
268
+ self.end_headers()
269
+ response = json.dumps({"status": "success", "message": "Files deleted successfully."})
270
+ self.wfile.write(response.encode('utf-8'))
271
+ else:
272
+ self.send_response(404)
273
+ self.end_headers()
274
+
275
+ def do_GET(self):
276
+ log("Full request path: {}".format(self.path)) # Log the full path for debugging
277
+ if self.path.startswith("/?code="):
278
+ auth_code = self.path.split('=')[1].split('&')[0]
279
+ auth_code = requests.utils.unquote(auth_code) # Decode if URL-encoded
280
+ log("Received authorization code: {}".format(auth_code))
281
+ if is_valid_authorization_code(auth_code):
282
+ try:
283
+ token_response = exchange_code_for_token(auth_code)
284
+ if 'access_token' not in token_response:
285
+ # Check for specific error message
286
+ if token_response.get("status") == "error":
287
+ self.send_response(400)
288
+ self.send_header('Content-type', 'text/html')
289
+ self.end_headers()
290
+ self.wfile.write(token_response["message"].encode())
291
+ return
292
+ # Handle other cases
293
+ raise ValueError("Access token not found in response.")
294
+ except Exception as e:
295
+ log("Error during token exchange: {}".format(e))
296
+ self.send_response(500)
297
+ self.send_header('Content-type', 'text/html')
298
+ self.end_headers()
299
+ self.wfile.write("An error occurred during authentication. Please try again.".encode())
300
+ else:
301
+ log("Token response: {}".format(token_response)) # Add this line
302
+ if 'access_token' in token_response:
303
+ with open(TOKEN_PATH, 'w') as token_file:
304
+ json.dump(token_response, token_file)
305
+ self.send_response(200)
306
+ self.send_header('Content-type', 'text/html')
307
+ self.end_headers()
308
+ self.wfile.write("Authentication successful. You can close this window now.".encode())
309
+ initiate_link_retrieval(config) # Pass config here
310
+ else:
311
+ log("Authentication failed with response: {}".format(token_response)) # Log the full response
312
+ if 'error' in token_response:
313
+ error_description = token_response.get('error_description', 'No description provided.')
314
+ log("Error details: {}".format(error_description)) # Log specific error details
315
+
316
+ # Provide user feedback based on the error
317
+ if token_response.get('error') == 'invalid_grant':
318
+ log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
319
+ check_invalid_grant_causes(auth_code)
320
+ clear_token_cache() # Clear the cache on invalid grant
321
+ user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
322
+ else:
323
+ user_message = "Authentication failed. Please check the logs for more details."
324
+
325
+ self.send_response(400)
326
+ self.send_header('Content-type', 'text/html')
327
+ self.end_headers()
328
+ self.wfile.write(user_message.encode())
329
+ shutdown_event.set() # Signal shutdown event after failed authentication
330
+ else:
331
+ log("Invalid authorization code format: {}".format(auth_code))
332
+ self.send_response(400)
333
+ self.send_header('Content-type', 'text/html')
334
+ self.end_headers()
335
+ self.wfile.write("Invalid authorization code format. Please try again.".encode())
336
+ shutdown_event.set() # Signal shutdown event after failed authentication
337
+ elif self.path == '/downloaded-emails':
338
+ self.send_response(200)
339
+ self._set_headers()
340
+ self.end_headers()
341
+ downloaded_emails = load_downloaded_emails()
342
+ response = json.dumps({"downloadedEmails": list(downloaded_emails)})
343
+ self.wfile.write(response.encode('utf-8'))
344
+ else:
345
+ self.send_response(200)
346
+ self.send_header('Access-Control-Allow-Origin', '*')
347
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
348
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
349
+ self.send_header('Content-type', 'text/html')
350
+ self.end_headers()
351
+ self.wfile.write(b'HTTPS server is running.')
352
+
353
+ def generate_self_signed_cert(cert_file, key_file):
354
+ log("Checking if certificate file exists: " + cert_file)
355
+ log("Checking if key file exists: " + key_file)
356
+
357
+ # Check if certificate exists and is not expired
358
+ cert_needs_regeneration = True
359
+ if os.path.exists(cert_file):
360
+ try:
361
+ # Check certificate expiration
362
+ check_cmd = ['openssl', 'x509', '-in', cert_file, '-checkend', '86400', '-noout'] # Check if expires in next 24 hours
363
+ result = subprocess.call(check_cmd)
364
+ if result == 0:
365
+ log("Certificate is still valid")
366
+ cert_needs_regeneration = False
367
+ else:
368
+ log("Certificate is expired or will expire soon")
369
+ # Delete expired certificate and key files
370
+ try:
371
+ if os.path.exists(cert_file):
372
+ os.remove(cert_file)
373
+ log("Deleted expired certificate file: {}".format(cert_file))
374
+ if os.path.exists(key_file):
375
+ os.remove(key_file)
376
+ log("Deleted expired key file: {}".format(key_file))
377
+ except Exception as e:
378
+ log("Error deleting expired certificate files: {}".format(e))
379
+ except Exception as e:
380
+ log("Error checking certificate expiration: {}".format(e))
381
+
382
+ if cert_needs_regeneration:
383
+ log("Generating self-signed SSL certificate...")
384
+ cmd = [
385
+ 'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
386
+ '-keyout', key_file,
387
+ '-out', cert_file,
388
+ '-days', '365',
389
+ '-sha256' # Use SHA-256 for better security
390
+ #'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
391
+ ]
392
+ try:
393
+ log("Running command: " + ' '.join(cmd))
394
+ result = subprocess.call(cmd)
395
+ log("Command finished with result: " + str(result))
396
+ if result != 0:
397
+ raise RuntimeError("Failed to generate self-signed certificate")
398
+
399
+ # Verify the certificate was generated correctly
400
+ verify_cmd = ['openssl', 'x509', '-in', cert_file, '-text', '-noout']
401
+ verify_result = subprocess.call(verify_cmd)
402
+ if verify_result != 0:
403
+ raise RuntimeError("Generated certificate verification failed")
404
+
405
+ log("Self-signed SSL certificate generated and verified successfully.")
406
+ except Exception as e:
407
+ log("Error generating self-signed certificate: {}".format(e))
408
+ raise
409
+
410
+ def run_server():
411
+ global httpd
412
+ try:
413
+ log("Attempting to start server on port " + str(server_port))
414
+ server_address = ('0.0.0.0', server_port) # Bind to all interfaces
415
+ httpd = HTTPServer(server_address, RequestHandler)
416
+ log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
417
+
418
+ if not os.path.exists(cert_file):
419
+ log("Error: Certificate file not found: " + cert_file)
420
+ if not os.path.exists(key_file):
421
+ log("Error: Key file not found: " + key_file)
422
+
423
+ httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
424
+ log("Starting HTTPS server on port {}".format(server_port))
425
+ httpd.serve_forever()
426
+ except Exception as e:
427
+ log("Error in serving: {}".format(e))
428
+ stop_server()
429
+
430
+ def stop_server():
431
+ global httpd
432
+ if httpd:
433
+ log("Stopping HTTPS server.")
434
+ httpd.shutdown()
435
+ httpd.server_close()
436
+ log("HTTPS server stopped.")
437
+ shutdown_event.set() # Signal shutdown event
438
+ bring_window_to_foreground() # Bring window to foreground after shutdown
439
+
440
+ def load_downloaded_emails():
441
+ downloaded_emails = set()
442
+ if os.path.exists(downloaded_emails_file):
443
+ with open(downloaded_emails_file, 'r') as file:
444
+ downloaded_emails = set(line.strip() for line in file)
445
+ log("Loaded downloaded emails: {}".format(downloaded_emails))
446
+ return downloaded_emails
447
+
448
+ def download_docx_files(links):
449
+ # Load the set of downloaded emails
450
+ # TODO (LOW-MEDIUM PRIORITY - CSV File Detection and Routing):
451
+ # PROBLEM: Downloaded files may include CSV files that need special handling and routing.
452
+ # Currently all files are treated the same regardless of extension.
453
+ #
454
+ # IMPLEMENTATION REQUIREMENTS:
455
+ # 1. File Extension Detection:
456
+ # - Check each downloaded file for .csv extension (case-insensitive)
457
+ # - Also check for common CSV variants: .txt, .tsv, .dat (based on content)
458
+ # - Handle files with multiple extensions like "report.csv.zip"
459
+ #
460
+ # 2. Content-Based Detection (Advanced):
461
+ # - For files without clear extensions, peek at content
462
+ # - Look for CSV patterns: comma-separated values, consistent column counts
463
+ # - Handle Excel files that might be CSV exports (.xlsx with CSV content)
464
+ #
465
+ # 3. CSV Routing Logic:
466
+ # - Move CSV files to dedicated CSV processing directory
467
+ # - Maintain file naming conventions for downstream processing
468
+ # - Log CSV file movements for audit trail
469
+ # - Preserve original file permissions and timestamps
470
+ #
471
+ # IMPLEMENTATION STEPS:
472
+ # 1. Add helper function detect_csv_files(downloaded_files) -> list
473
+ # 2. Add helper function move_csv_to_processing_dir(csv_file, destination_dir)
474
+ # 3. Add configuration for CSV destination directory in config file
475
+ # 4. Update this function to call CSV detection and routing after download
476
+ # 5. Add error handling for file movement failures
477
+ # 6. Add logging for all CSV file operations
478
+ #
479
+ # CONFIGURATION NEEDED:
480
+ # - config['csv_processing_dir']: Where to move detected CSV files
481
+ # - config['csv_file_extensions']: List of extensions to treat as CSV
482
+ # - config['csv_content_detection']: Boolean to enable content-based detection
483
+ #
484
+ # ERROR HANDLING:
485
+ # - Handle permission errors when moving files
486
+ # - Handle disk space issues
487
+ # - Gracefully handle corrupted or locked files
488
+ # - Provide fallback options when CSV directory is unavailable
489
+ #
490
+ # TESTING SCENARIOS:
491
+ # - Mixed file types: .docx, .csv, .pdf in same download batch
492
+ # - CSV files with unusual extensions (.txt, .dat)
493
+ # - Large CSV files (>100MB)
494
+ # - CSV files in ZIP archives
495
+ #
496
+ # FILES TO MODIFY: This file (download_docx_files function)
497
+ # RELATED: May need updates to CSV processing modules that expect files in specific locations
498
+ downloaded_emails = load_downloaded_emails()
499
+
500
+ for link in links:
501
+ try:
502
+ url = link.get('url', '')
503
+ filename = link.get('filename', '')
504
+
505
+ # Log the variables to debug
506
+ log("Processing link: url='{}', filename='{}'".format(url, filename))
507
+
508
+ # Skip if email already downloaded
509
+ if filename in downloaded_emails:
510
+ log("Skipping already downloaded email: {}".format(filename))
511
+ continue
512
+
513
+ log("Downloading .docx file from URL: {}".format(url))
514
+ response = requests.get(url, verify=False) # Set verify to False for self-signed certs
515
+ if response.status_code == 200:
516
+ file_path = os.path.join(local_storage_path, filename)
517
+ with open(file_path, 'wb') as file:
518
+ file.write(response.content)
519
+ log("Downloaded .docx file: {}".format(filename))
520
+ # Add to the set and save the updated list
521
+ downloaded_emails.add(filename)
522
+ with open(downloaded_emails_file, 'a') as file:
523
+ file.write(filename + '\n')
524
+ else:
525
+ log("Failed to download .docx file from URL: {}. Status code: {}".format(url, response.status_code))
526
+ except Exception as e:
527
+ log("Error downloading .docx file from URL: {}. Error: {}".format(url, e))
528
+
529
+ def open_browser_with_executable(url, browser_path=None):
530
+ try:
531
+ if browser_path:
532
+ log("Attempting to open URL with provided executable: {} {}".format(browser_path, url))
533
+ process = subprocess.Popen([browser_path, url], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
534
+ stdout, stderr = process.communicate()
535
+ if process.returncode == 0:
536
+ log("Browser opened with provided executable path using subprocess.Popen.")
537
+ else:
538
+ log("Browser failed to open using subprocess.Popen. Return code: {}. Stderr: {}".format(process.returncode, stderr))
539
+ else:
540
+ log("No browser path provided. Attempting to open URL with default browser: {}".format(url))
541
+ webbrowser.open(url)
542
+ log("Default browser opened.")
543
+ except Exception as e:
544
+ log("Failed to open browser: {}".format(e))
545
+
546
+ def initiate_link_retrieval(config):
547
+ log("Initiating browser via implicit GET.")
548
+ url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
549
+ open_browser_with_executable(url_get)
550
+
551
+ log("Preparing POST call.")
552
+ url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
553
+ downloaded_emails = list(load_downloaded_emails())
554
+ payload = {
555
+ "downloadedEmails": downloaded_emails
556
+ }
557
+
558
+ access_token = get_access_token()
559
+ if not access_token:
560
+ log("Access token not found. Please authenticate first.")
561
+ shutdown_event.set() # Signal shutdown event if token is not found
562
+ return
563
+
564
+ # Inspect the token to check its validity and permissions
565
+ token_info = inspect_token(access_token)
566
+ if token_info is None:
567
+ log("Access token is invalid. Please re-authenticate.")
568
+ shutdown_event.set() # Signal shutdown event if token is invalid
569
+ return
570
+
571
+ # Proceed with the rest of the function if the token is valid
572
+ headers = {
573
+ 'Authorization': 'Bearer {}'.format(access_token),
574
+ 'Content-Type': 'application/json'
575
+ }
576
+
577
+ log("Request headers: {}".format(headers))
578
+ log("Request payload: {}".format(payload))
579
+
580
+ handle_post_response(url, payload, headers)
581
+
582
+ def handle_post_response(url, payload, headers):
583
+ try:
584
+ response = requests.post(url, json=payload, headers=headers)
585
+ log("Response status code: {}".format(response.status_code))
586
+ log("Response body: {}".format(response.text))
587
+
588
+ if response.status_code == 200:
589
+ response_data = response.json()
590
+ log("Parsed response data: {}".format(response_data)) # Log the parsed response data
591
+ if response_data.get("status") == "error":
592
+ log("Error message from server: {}".format(response_data.get("message")))
593
+ print("Error: {}".format(response_data.get("message")))
594
+ shutdown_event.set() # Signal shutdown event after error
595
+ else:
596
+ log("Link retrieval initiated successfully.")
597
+ elif response.status_code == 401:
598
+ log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
599
+ # Inspect the token to log its details
600
+ token_info = inspect_token(headers['Authorization'].split(' ')[1])
601
+ log("Token details: {}".format(token_info))
602
+ shutdown_event.set()
603
+ elif response.status_code == 403:
604
+ log("Forbidden access. Ensure that the OAuth client has the correct permissions. Response body: {}".format(response.text))
605
+ shutdown_event.set()
606
+ elif response.status_code == 404:
607
+ log("Not Found. Verify the URL and ensure the Apps Script is deployed correctly. Response body: {}".format(response.text))
608
+ shutdown_event.set()
609
+ else:
610
+ log("Failed to initiate link retrieval. Unexpected status code: {}. Response body: {}".format(response.status_code, response.text))
611
+ shutdown_event.set()
612
+ except requests.exceptions.RequestException as e:
613
+ log("RequestException during link retrieval initiation: {}".format(e))
614
+ shutdown_event.set()
615
+ except Exception as e:
616
+ log("Unexpected error during link retrieval initiation: {}".format(e))
617
+ shutdown_event.set()
618
+
619
+ def inspect_token(access_token):
620
+ info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
621
+ try:
622
+ response = requests.get(info_url)
623
+ log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
624
+
625
+ if response.status_code == 200:
626
+ return response.json()
627
+ else:
628
+ log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
629
+ # Check for invalid token
630
+ if response.status_code == 400 and "invalid_token" in response.text:
631
+ log("Access token is invalid. Deleting token.json and stopping the server.")
632
+ delete_token_file() # Delete the token.json file
633
+ print("Access token is invalid. Please re-authenticate and restart the server.")
634
+ stop_server() # Stop the server
635
+ return None # Return None for invalid tokens
636
+ return None # Return None for other invalid tokens
637
+ except Exception as e:
638
+ log("Exception during token inspection: {}".format(e))
639
+ return None
640
+
641
+ def delete_token_file():
642
+ try:
643
+ if os.path.exists(TOKEN_PATH):
644
+ os.remove(TOKEN_PATH)
645
+ log("Deleted token.json successfully.")
646
+ else:
647
+ log("token.json does not exist.")
648
+ except Exception as e:
649
+ log("Error deleting token.json: {}".format(e))
650
+
651
+ def signal_handler(sig, frame):
652
+ log("Signal received: {}. Initiating shutdown.".format(sig))
653
+ stop_server()
654
+ sys.exit(0)
655
+
656
+ def auth_and_retrieval():
657
+ access_token = get_access_token()
658
+ if not access_token:
659
+ log("Access token not found or expired. Please authenticate first.")
660
+ #print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
661
+ auth_url = get_authorization_url()
662
+ #print(auth_url)
663
+ open_browser_with_executable(auth_url)
664
+ shutdown_event.wait() # Wait for the shutdown event to be set after authentication
665
+ else:
666
+ log("Access token found. Proceeding.")
667
+ initiate_link_retrieval(config) # Pass config here
668
+ shutdown_event.wait() # Wait for the shutdown event to be set
669
+
670
+ def is_valid_authorization_code(auth_code):
671
+ # Check if the authorization code is not None and is a non-empty string
672
+ if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
673
+ return True
674
+ log("Invalid authorization code format: {}".format(auth_code))
675
+ return False
676
+
677
+ def clear_token_cache():
678
+ if os.path.exists(TOKEN_PATH):
679
+ os.remove(TOKEN_PATH)
680
+ log("Cleared token cache.")
681
+
682
+ def check_invalid_grant_causes(auth_code):
683
+ # TODO Implement this function in the future to check for common causes of invalid_grant error
684
+ # Log potential causes for invalid_grant
685
+ log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
686
+ # Example checks (you can expand this based on your needs)
687
+ """
688
+ if is_code_used(auth_code):
689
+ log("Authorization code has already been used.")
690
+ if not is_redirect_uri_correct():
691
+ log("Redirect URI does not match the registered URI.")
692
+ """
693
+
694
+ if __name__ == "__main__":
695
+ signal.signal(signal.SIGINT, signal_handler)
696
+ signal.signal(signal.SIGTERM, signal_handler)
697
+
698
+ try:
699
+ # Generate SSL certificate if it doesn't exist
700
+ generate_self_signed_cert(cert_file, key_file)
701
+
702
+ from threading import Thread
703
+ log("Starting server thread.")
704
+ server_thread = Thread(target=run_server)
705
+ server_thread.daemon = True
706
+ server_thread.start()
707
+
708
+ auth_and_retrieval()
709
+
710
+ log("Stopping HTTPS server.")
711
+ stop_server() # Ensure the server is stopped
712
+ log("Waiting for server thread to finish.")
713
+ server_thread.join() # Wait for the server thread to finish
714
+ except KeyboardInterrupt:
715
+ log("KeyboardInterrupt received, stopping server.")
716
+ stop_server()
717
+ sys.exit(0)
718
+ except Exception as e:
719
+ log("An error occurred: {}".format(e))
720
+ stop_server()
654
721
  sys.exit(1)