medicafe 0.251027.2__tar.gz → 0.251027.3__tar.gz
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.
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/__init__.py +1 -1
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/__init__.py +1 -1
- medicafe-0.251027.3/MediCafe/error_reporter.py +603 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Gmail.py +65 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/__init__.py +1 -1
- {medicafe-0.251027.2/medicafe.egg-info → medicafe-0.251027.3}/PKG-INFO +1 -1
- {medicafe-0.251027.2 → medicafe-0.251027.3/medicafe.egg-info}/PKG-INFO +1 -1
- {medicafe-0.251027.2 → medicafe-0.251027.3}/setup.py +1 -1
- medicafe-0.251027.2/MediCafe/error_reporter.py +0 -452
- {medicafe-0.251027.2 → medicafe-0.251027.3}/LICENSE +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MANIFEST.in +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Notepad_Utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_Preprocessor_lib.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_UI.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_debug.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/clear_cache.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/crash_diagnostic.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/f_drive_diagnostic.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/full_debug_suite.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/process_csvs.bat +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/update_json.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/__main__.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/api_core.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/api_factory.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/api_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/core_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/deductible_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/logging_config.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/smart_import.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediCafe/submission_index.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Charges.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Display_Utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/openssl.cnf +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/test.py +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/MediLink/webapp.html +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/README.md +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.251027.2 → medicafe-0.251027.3}/setup.cfg +0 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import traceback
|
|
11
|
+
|
|
12
|
+
from email.mime.application import MIMEApplication
|
|
13
|
+
from email.mime.multipart import MIMEMultipart
|
|
14
|
+
from email.mime.text import MIMEText
|
|
15
|
+
|
|
16
|
+
from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
|
|
17
|
+
from MediLink.MediLink_Gmail import get_access_token
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _safe_ascii(text):
|
|
21
|
+
try:
|
|
22
|
+
if text is None:
|
|
23
|
+
return ''
|
|
24
|
+
if isinstance(text, bytes):
|
|
25
|
+
try:
|
|
26
|
+
text = text.decode('ascii', 'ignore')
|
|
27
|
+
except Exception:
|
|
28
|
+
text = text.decode('utf-8', 'ignore')
|
|
29
|
+
else:
|
|
30
|
+
text = str(text)
|
|
31
|
+
return text.encode('ascii', 'ignore').decode('ascii', 'ignore')
|
|
32
|
+
except Exception:
|
|
33
|
+
return ''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _tail_file(path, max_lines):
|
|
37
|
+
lines = []
|
|
38
|
+
try:
|
|
39
|
+
with open(path, 'r') as f:
|
|
40
|
+
for line in f:
|
|
41
|
+
lines.append(line)
|
|
42
|
+
if len(lines) > max_lines:
|
|
43
|
+
lines.pop(0)
|
|
44
|
+
return ''.join(lines)
|
|
45
|
+
except Exception:
|
|
46
|
+
return ''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_latest_log_path(local_storage_path):
|
|
50
|
+
try:
|
|
51
|
+
files = []
|
|
52
|
+
for name in os.listdir(local_storage_path or '.'):
|
|
53
|
+
if name.startswith('Log_') and name.endswith('.log'):
|
|
54
|
+
files.append(os.path.join(local_storage_path, name))
|
|
55
|
+
if not files:
|
|
56
|
+
return None
|
|
57
|
+
files.sort(key=lambda p: os.path.getmtime(p))
|
|
58
|
+
return files[-1]
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _redact(text):
|
|
64
|
+
# Best-effort ASCII redaction: mask common secrets in logs and JSON
|
|
65
|
+
try:
|
|
66
|
+
text = _safe_ascii(text)
|
|
67
|
+
import re
|
|
68
|
+
patterns = [
|
|
69
|
+
# SSN-like
|
|
70
|
+
(r'\b(\d{3}-?\d{2}-?\d{4})\b', '***-**-****'),
|
|
71
|
+
# 9-11 digit numeric IDs
|
|
72
|
+
(r'\b(\d{9,11})\b', '*********'),
|
|
73
|
+
# Authorization headers
|
|
74
|
+
(r'Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Authorization: Bearer ***'),
|
|
75
|
+
(r'Authorization:\s*[^\n\r]+', 'Authorization: ***'),
|
|
76
|
+
# JSON token fields
|
|
77
|
+
(r'("access_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
78
|
+
(r'("refresh_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
79
|
+
(r'("id_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
80
|
+
(r'("X-Auth-Token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
81
|
+
# URL query params: token=..., access_token=..., auth=...
|
|
82
|
+
(r'(token|access_token|auth|authorization)=([^&\s]+)', r'\1=***'),
|
|
83
|
+
# Bearer fragments in JSON or text
|
|
84
|
+
(r'Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Bearer ***'),
|
|
85
|
+
]
|
|
86
|
+
for pat, rep in patterns:
|
|
87
|
+
text = re.sub(pat, rep, text)
|
|
88
|
+
return text
|
|
89
|
+
except Exception:
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _ensure_dir(path):
|
|
94
|
+
try:
|
|
95
|
+
if not os.path.exists(path):
|
|
96
|
+
os.makedirs(path)
|
|
97
|
+
return True
|
|
98
|
+
except Exception:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Resolve a writable queue directory with fallback to ./reports_queue
|
|
103
|
+
def _resolve_queue_dir(medi_config):
|
|
104
|
+
try:
|
|
105
|
+
local_storage_path = medi_config.get('local_storage_path', '.') if isinstance(medi_config, dict) else '.'
|
|
106
|
+
except Exception:
|
|
107
|
+
local_storage_path = '.'
|
|
108
|
+
primary = os.path.join(local_storage_path, 'reports_queue')
|
|
109
|
+
if _ensure_dir(primary):
|
|
110
|
+
return primary
|
|
111
|
+
fallback = os.path.join('.', 'reports_queue')
|
|
112
|
+
if _ensure_dir(fallback):
|
|
113
|
+
try:
|
|
114
|
+
mc_log("Queue directory fallback to ./reports_queue due to path/permission issue.", level="WARNING")
|
|
115
|
+
print("Falling back to ./reports_queue for support bundles (check local_storage_path permissions).")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
return fallback
|
|
119
|
+
return primary
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build_support_zip(zip_path, local_storage_path, max_log_lines, traceback_text, include_winscp, meta):
|
|
123
|
+
latest_log = _get_latest_log_path(local_storage_path)
|
|
124
|
+
log_tail = _tail_file(latest_log, max_log_lines) if latest_log else ''
|
|
125
|
+
log_tail = _redact(log_tail)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
|
129
|
+
z.writestr('meta.json', json.dumps(meta, ensure_ascii=True, indent=2))
|
|
130
|
+
if latest_log and log_tail:
|
|
131
|
+
z.writestr('log_tail.txt', log_tail)
|
|
132
|
+
if traceback_text:
|
|
133
|
+
z.writestr('traceback.txt', _redact(traceback_text))
|
|
134
|
+
if include_winscp:
|
|
135
|
+
upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
|
|
136
|
+
download_log = os.path.join(local_storage_path, 'winscp_download.log')
|
|
137
|
+
winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
|
|
138
|
+
if winscp_logs:
|
|
139
|
+
latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
|
|
140
|
+
winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
|
|
141
|
+
winscp_tail = _redact(winscp_tail)
|
|
142
|
+
if winscp_tail:
|
|
143
|
+
z.writestr('winscp_log_tail.txt', winscp_tail)
|
|
144
|
+
return True
|
|
145
|
+
except Exception as e:
|
|
146
|
+
mc_log('Error creating support bundle at {}: {}'.format(zip_path, e), level='ERROR')
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
# Centralized self-healing helpers for email reporting
|
|
150
|
+
def _attempt_gmail_reauth_interactive(max_wait_seconds=120):
|
|
151
|
+
"""
|
|
152
|
+
Delegate to MediLink.MediLink_Gmail.ensure_authenticated_for_gmail_send to
|
|
153
|
+
reuse existing OAuth/server logic without duplicating implementations.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
from MediLink.MediLink_Gmail import ensure_authenticated_for_gmail_send
|
|
157
|
+
return bool(ensure_authenticated_for_gmail_send(max_wait_seconds=max_wait_seconds))
|
|
158
|
+
except Exception as e:
|
|
159
|
+
try:
|
|
160
|
+
mc_log("Failed to initiate Gmail re-authorization: {0}".format(e), level="ERROR")
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
# _compute_report_id removed (unused)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def collect_support_bundle(include_traceback=True, max_log_lines=500):
|
|
169
|
+
config, _ = load_configuration()
|
|
170
|
+
medi = config.get('MediLink_Config', {})
|
|
171
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
172
|
+
queue_dir = _resolve_queue_dir(medi)
|
|
173
|
+
|
|
174
|
+
stamp = time.strftime('%Y%m%d_%H%M%S')
|
|
175
|
+
zip_path = os.path.join(queue_dir, 'support_report_{}.zip'.format(stamp))
|
|
176
|
+
|
|
177
|
+
traceback_txt = ''
|
|
178
|
+
if include_traceback:
|
|
179
|
+
try:
|
|
180
|
+
trace_path = os.path.join(local_storage_path, 'traceback.txt')
|
|
181
|
+
if os.path.exists(trace_path):
|
|
182
|
+
with open(trace_path, 'r') as tf:
|
|
183
|
+
traceback_txt = tf.read()
|
|
184
|
+
except Exception:
|
|
185
|
+
traceback_txt = ''
|
|
186
|
+
|
|
187
|
+
meta = {
|
|
188
|
+
'app_version': _safe_ascii(_get_version()),
|
|
189
|
+
'python_version': _safe_ascii(sys.version.split(' ')[0]),
|
|
190
|
+
'platform': _safe_ascii(platform.platform()),
|
|
191
|
+
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
192
|
+
'error_summary': _safe_ascii(_first_line(traceback_txt)),
|
|
193
|
+
'traceback_present': bool(traceback_txt),
|
|
194
|
+
'config_flags': {
|
|
195
|
+
'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
|
|
196
|
+
'test_mode': bool(medi.get('TestMode', False))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, traceback_txt, True, meta)
|
|
201
|
+
return zip_path if ok else None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def collect_test_support_bundle(max_log_lines=500):
|
|
205
|
+
"""
|
|
206
|
+
Build a support bundle using the latest available logs and a placeholder
|
|
207
|
+
(fake) traceback to exercise the reporting pipeline without exposing
|
|
208
|
+
real exception data.
|
|
209
|
+
|
|
210
|
+
Returns absolute path to the created ZIP, or None on failure.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
config, _ = load_configuration()
|
|
214
|
+
medi = config.get('MediLink_Config', {})
|
|
215
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
216
|
+
queue_dir = _resolve_queue_dir(medi)
|
|
217
|
+
|
|
218
|
+
stamp = time.strftime('%Y%m%d_%H%M%S')
|
|
219
|
+
zip_path = os.path.join(queue_dir, 'support_report_TEST_{}.zip'.format(stamp))
|
|
220
|
+
|
|
221
|
+
# Build a placeholder traceback - ASCII-only, no real data
|
|
222
|
+
fake_tb = (
|
|
223
|
+
"Traceback (most recent call last):\n"
|
|
224
|
+
" File \"MediCafe/test_runner.py\", line 42, in <module>\n"
|
|
225
|
+
" File \"MediCafe/error_reporter.py\", line 123, in simulate_error\n"
|
|
226
|
+
"Exception: This is a TEST placeholder traceback for pipeline verification only.\n"
|
|
227
|
+
"-- No real patient or PHI data is included. --\n"
|
|
228
|
+
)
|
|
229
|
+
meta = {
|
|
230
|
+
'app_version': _safe_ascii(_get_version()),
|
|
231
|
+
'python_version': _safe_ascii(sys.version.split(' ')[0]),
|
|
232
|
+
'platform': _safe_ascii(platform.platform()),
|
|
233
|
+
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
234
|
+
'error_summary': 'TEST: Placeholder traceback',
|
|
235
|
+
'traceback_present': True,
|
|
236
|
+
'config_flags': {
|
|
237
|
+
'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
|
|
238
|
+
'test_mode': True
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, fake_tb, True, meta)
|
|
242
|
+
return zip_path if ok else None
|
|
243
|
+
except Exception as e:
|
|
244
|
+
try:
|
|
245
|
+
mc_log('Error creating TEST support bundle: {}'.format(e), level='ERROR')
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _first_line(text):
|
|
252
|
+
try:
|
|
253
|
+
for line in (text or '').splitlines():
|
|
254
|
+
line = line.strip()
|
|
255
|
+
if line:
|
|
256
|
+
return line[:200]
|
|
257
|
+
return ''
|
|
258
|
+
except Exception:
|
|
259
|
+
return ''
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _get_version():
|
|
263
|
+
try:
|
|
264
|
+
from MediCafe import __version__
|
|
265
|
+
return __version__
|
|
266
|
+
except Exception:
|
|
267
|
+
return 'unknown'
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
|
|
271
|
+
try:
|
|
272
|
+
config, _ = load_configuration()
|
|
273
|
+
medi = config.get('MediLink_Config', {})
|
|
274
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
275
|
+
trace_path = os.path.join(local_storage_path, 'traceback.txt')
|
|
276
|
+
text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
|
277
|
+
text = _redact(text)
|
|
278
|
+
with open(trace_path, 'w') as f:
|
|
279
|
+
f.write(text)
|
|
280
|
+
print("An error occurred. A traceback was saved to {}".format(trace_path))
|
|
281
|
+
except Exception:
|
|
282
|
+
try:
|
|
283
|
+
mc_log('Failed to capture traceback to file', level='WARNING')
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def submit_support_bundle_email(zip_path=None, include_traceback=True):
|
|
288
|
+
if not zip_path:
|
|
289
|
+
zip_path = collect_support_bundle(include_traceback)
|
|
290
|
+
if not zip_path:
|
|
291
|
+
mc_log("Failed to create bundle.", level="ERROR")
|
|
292
|
+
try:
|
|
293
|
+
print("Failed to create support bundle. Ensure 'MediLink_Config.local_storage_path' is writable and logs exist.")
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
return False
|
|
297
|
+
bundle_size = os.path.getsize(zip_path)
|
|
298
|
+
config, _ = load_configuration()
|
|
299
|
+
email_config = config.get('MediLink_Config', {}).get('error_reporting', {}).get('email', {})
|
|
300
|
+
# Determine max size from config with default 1.5MB
|
|
301
|
+
try:
|
|
302
|
+
max_bytes = int(email_config.get('max_bundle_bytes', 1572864))
|
|
303
|
+
except Exception:
|
|
304
|
+
max_bytes = 1572864
|
|
305
|
+
if bundle_size > max_bytes:
|
|
306
|
+
mc_log("Bundle too large ({} bytes > {} bytes) - leaving in queue.".format(bundle_size, max_bytes), level="WARNING")
|
|
307
|
+
try:
|
|
308
|
+
print("Bundle too large to email ({} KB > {} KB). Left in 'reports_queue'. Path: {}".format(int(bundle_size/1024), int(max_bytes/1024), zip_path))
|
|
309
|
+
print("Attempting to create and send a smaller LITE bundle (reduced logs, no traceback).")
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
# Attempt a smaller bundle automatically
|
|
313
|
+
lite_zip = collect_support_bundle_lite(max_log_lines=200)
|
|
314
|
+
if lite_zip and os.path.exists(lite_zip):
|
|
315
|
+
try:
|
|
316
|
+
lite_size = os.path.getsize(lite_zip)
|
|
317
|
+
except Exception:
|
|
318
|
+
lite_size = -1
|
|
319
|
+
if 0 < lite_size <= max_bytes:
|
|
320
|
+
return submit_support_bundle_email(zip_path=lite_zip, include_traceback=False)
|
|
321
|
+
else:
|
|
322
|
+
try:
|
|
323
|
+
print("LITE bundle is still too large ({} KB).".format(int(max(lite_size, 0)/1024)))
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
try:
|
|
327
|
+
print("Tip: Reduce log size or increase 'MediLink_Config.error_reporting.email.max_bundle_bytes'.")
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
return False
|
|
331
|
+
# Feature is always available; proceed if recipients and token are available
|
|
332
|
+
# Normalize and validate recipients
|
|
333
|
+
to_emails = _normalize_recipients(email_config.get('to', []))
|
|
334
|
+
if not to_emails:
|
|
335
|
+
mc_log("No valid recipients configured in error_reporting.email.to", level="ERROR")
|
|
336
|
+
try:
|
|
337
|
+
print("No recipients configured. Set 'MediLink_Config.error_reporting.email.to' to one or more email addresses.")
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
return False
|
|
341
|
+
subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
|
|
342
|
+
access_token = get_access_token()
|
|
343
|
+
if not access_token:
|
|
344
|
+
mc_log("No access token - attempting Gmail re-authorization.", level="ERROR")
|
|
345
|
+
try:
|
|
346
|
+
print("No Gmail token found. Starting re-authorization...")
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
if _attempt_gmail_reauth_interactive():
|
|
350
|
+
access_token = get_access_token()
|
|
351
|
+
if not access_token:
|
|
352
|
+
try:
|
|
353
|
+
print("Authentication incomplete. Please finish Gmail consent, then retry.")
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
return False
|
|
357
|
+
mc_log("Building email...", level="INFO")
|
|
358
|
+
msg = MIMEMultipart()
|
|
359
|
+
msg['To'] = ', '.join(to_emails)
|
|
360
|
+
msg['Subject'] = '{} - {}'.format(subject_prefix, time.strftime('%Y%m%d_%H%M%S'))
|
|
361
|
+
with open(zip_path, 'rb') as f:
|
|
362
|
+
attach = MIMEApplication(f.read(), _subtype='zip')
|
|
363
|
+
attach.add_header('Content-Disposition', 'attachment', filename=os.path.basename(zip_path))
|
|
364
|
+
msg.attach(attach)
|
|
365
|
+
body = "Error report attached."
|
|
366
|
+
msg.attach(MIMEText(body, 'plain'))
|
|
367
|
+
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
|
|
368
|
+
mc_log("Sending report...", level="INFO")
|
|
369
|
+
headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
|
|
370
|
+
data = {'raw': raw}
|
|
371
|
+
try:
|
|
372
|
+
resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
mc_log("Network error during Gmail send: {0}".format(e), level="ERROR")
|
|
375
|
+
try:
|
|
376
|
+
print("Network error while sending report: {0}".format(e))
|
|
377
|
+
print("Check internet connectivity or proxy settings and retry.")
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
return False
|
|
381
|
+
if resp.status_code == 200:
|
|
382
|
+
mc_log("Report sent successfully!", level="INFO")
|
|
383
|
+
try:
|
|
384
|
+
os.remove(zip_path)
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
return True
|
|
388
|
+
else:
|
|
389
|
+
# Handle auth errors by prompting re-consent using existing OAuth helpers, then retry once
|
|
390
|
+
if resp.status_code in (401, 403):
|
|
391
|
+
mc_log("Gmail send unauthorized ({}). Attempting re-authorization and retry.".format(resp.status_code), level="WARNING")
|
|
392
|
+
try:
|
|
393
|
+
print("Gmail permission issue detected ({}). Starting re-authorization...".format(resp.status_code))
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
if _attempt_gmail_reauth_interactive():
|
|
397
|
+
new_token = get_access_token()
|
|
398
|
+
if new_token:
|
|
399
|
+
headers['Authorization'] = 'Bearer {}'.format(new_token)
|
|
400
|
+
try:
|
|
401
|
+
resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
mc_log("Network error on retry: {0}".format(e), level="ERROR")
|
|
404
|
+
try:
|
|
405
|
+
print("Network error on retry: {0}".format(e))
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
return False
|
|
409
|
+
if resp.status_code == 200:
|
|
410
|
+
mc_log("Report sent successfully after re-authorization!", level="INFO")
|
|
411
|
+
try:
|
|
412
|
+
os.remove(zip_path)
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
return True
|
|
416
|
+
# Map common Gmail errors to actionable hints
|
|
417
|
+
hint = ''
|
|
418
|
+
try:
|
|
419
|
+
body = resp.json()
|
|
420
|
+
err = body.get('error', {}) if isinstance(body, dict) else {}
|
|
421
|
+
status = (err.get('status') or '').upper()
|
|
422
|
+
message = err.get('message') or ''
|
|
423
|
+
reasons = ','.join([e.get('reason') for e in err.get('errors', []) if isinstance(e, dict) and e.get('reason')])
|
|
424
|
+
if 'RATELIMIT' in status or 'rateLimitExceeded' in reasons:
|
|
425
|
+
hint = 'Quota exceeded. Wait and retry later.'
|
|
426
|
+
elif 'DAILY' in status or 'dailyLimitExceeded' in reasons:
|
|
427
|
+
hint = 'Daily quota exceeded. Try again tomorrow.'
|
|
428
|
+
elif status in ('PERMISSION_DENIED', 'FORBIDDEN'):
|
|
429
|
+
hint = 'Permissions insufficient. Re-authorize Gmail with required scopes.'
|
|
430
|
+
elif status == 'INVALID_ARGUMENT':
|
|
431
|
+
hint = 'Invalid request. Check recipient emails and attachment size.'
|
|
432
|
+
elif status == 'UNAUTHENTICATED':
|
|
433
|
+
hint = 'Authentication required. Re-authorize and retry.'
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
mc_log("Failed to send: {} - {}".format(resp.status_code, _redact(resp.text)), level="ERROR")
|
|
437
|
+
try:
|
|
438
|
+
base_msg = "Failed to send report: HTTP {}.".format(resp.status_code)
|
|
439
|
+
if hint:
|
|
440
|
+
base_msg += " Hint: {}".format(hint)
|
|
441
|
+
print(base_msg)
|
|
442
|
+
print("The bundle remains in 'reports_queue'. See latest log for details.")
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
# Preserve bundle in queue for manual retry
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _normalize_recipients(to_field):
|
|
450
|
+
try:
|
|
451
|
+
# Flatten to a list of strings
|
|
452
|
+
if isinstance(to_field, str):
|
|
453
|
+
separators_normalized = to_field.replace(';', ',').replace('\n', ',').replace('\r', ',')
|
|
454
|
+
candidates = [p.strip() for p in separators_normalized.split(',')]
|
|
455
|
+
elif isinstance(to_field, list):
|
|
456
|
+
candidates = [str(p).strip() for p in to_field]
|
|
457
|
+
else:
|
|
458
|
+
candidates = []
|
|
459
|
+
# Basic email regex: local@domain.tld
|
|
460
|
+
import re
|
|
461
|
+
email_re = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
|
462
|
+
valid = []
|
|
463
|
+
for addr in candidates:
|
|
464
|
+
if not addr:
|
|
465
|
+
continue
|
|
466
|
+
if email_re.match(addr):
|
|
467
|
+
valid.append(addr)
|
|
468
|
+
else:
|
|
469
|
+
try:
|
|
470
|
+
mc_log("Invalid email recipient skipped: {}".format(addr), level="WARNING")
|
|
471
|
+
except Exception:
|
|
472
|
+
pass
|
|
473
|
+
return valid
|
|
474
|
+
except Exception:
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def collect_support_bundle_lite(max_log_lines=200):
|
|
479
|
+
"""Create a smaller 'lite' support bundle with reduced logs and no traceback/winscp logs."""
|
|
480
|
+
try:
|
|
481
|
+
config, _ = load_configuration()
|
|
482
|
+
medi = config.get('MediLink_Config', {})
|
|
483
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
484
|
+
queue_dir = _resolve_queue_dir(medi)
|
|
485
|
+
stamp = time.strftime('%Y%m%d_%H%M%S')
|
|
486
|
+
zip_path = os.path.join(queue_dir, 'support_report_LITE_{}.zip'.format(stamp))
|
|
487
|
+
meta = {
|
|
488
|
+
'app_version': _safe_ascii(_get_version()),
|
|
489
|
+
'python_version': _safe_ascii(sys.version.split(' ')[0]),
|
|
490
|
+
'platform': _safe_ascii(platform.platform()),
|
|
491
|
+
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
492
|
+
'error_summary': 'LITE: No traceback included',
|
|
493
|
+
'traceback_present': False,
|
|
494
|
+
'config_flags': {
|
|
495
|
+
'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
|
|
496
|
+
'test_mode': bool(medi.get('TestMode', False))
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, None, False, meta)
|
|
500
|
+
return zip_path if ok else None
|
|
501
|
+
except Exception:
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def list_queued_bundles():
|
|
506
|
+
try:
|
|
507
|
+
config, _ = load_configuration()
|
|
508
|
+
medi = config.get('MediLink_Config', {})
|
|
509
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
510
|
+
primary = os.path.join(local_storage_path, 'reports_queue')
|
|
511
|
+
fallback = os.path.join('.', 'reports_queue')
|
|
512
|
+
files = []
|
|
513
|
+
for q in (primary, fallback):
|
|
514
|
+
try:
|
|
515
|
+
if os.path.isdir(q):
|
|
516
|
+
files.extend([os.path.join(q, f) for f in os.listdir(q) if f.endswith('.zip')])
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
files = sorted(set(files))
|
|
520
|
+
return files
|
|
521
|
+
except Exception:
|
|
522
|
+
return []
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def submit_all_queued_bundles():
|
|
526
|
+
sent = 0
|
|
527
|
+
failed = 0
|
|
528
|
+
try:
|
|
529
|
+
queued = list_queued_bundles()
|
|
530
|
+
for z in queued:
|
|
531
|
+
try:
|
|
532
|
+
ok = submit_support_bundle_email(zip_path=z, include_traceback=False)
|
|
533
|
+
if ok:
|
|
534
|
+
sent += 1
|
|
535
|
+
else:
|
|
536
|
+
failed += 1
|
|
537
|
+
except Exception:
|
|
538
|
+
failed += 1
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
return sent, failed
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def delete_all_queued_bundles():
|
|
545
|
+
deleted = 0
|
|
546
|
+
try:
|
|
547
|
+
for z in list_queued_bundles():
|
|
548
|
+
try:
|
|
549
|
+
os.remove(z)
|
|
550
|
+
deleted += 1
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
except Exception:
|
|
554
|
+
pass
|
|
555
|
+
return deleted
|
|
556
|
+
|
|
557
|
+
def email_error_report_flow():
|
|
558
|
+
try:
|
|
559
|
+
sent = submit_support_bundle_email(zip_path=None, include_traceback=True)
|
|
560
|
+
return 0 if sent else 1
|
|
561
|
+
except Exception as e:
|
|
562
|
+
mc_log("[ERROR] Exception during email report flow: {0}".format(e), level="ERROR")
|
|
563
|
+
try:
|
|
564
|
+
print("Unexpected error while sending error report: {0}".format(e))
|
|
565
|
+
except Exception:
|
|
566
|
+
pass
|
|
567
|
+
return 1
|
|
568
|
+
|
|
569
|
+
def email_test_error_report_flow():
|
|
570
|
+
"""
|
|
571
|
+
Create and send a TEST error report bundle, containing a placeholder
|
|
572
|
+
traceback and latest log tails. Intended for troubleshooting the
|
|
573
|
+
submission pipeline only.
|
|
574
|
+
"""
|
|
575
|
+
try:
|
|
576
|
+
zip_path = collect_test_support_bundle()
|
|
577
|
+
if not zip_path:
|
|
578
|
+
try:
|
|
579
|
+
print("Failed to create TEST support bundle. Ensure 'MediLink_Config.local_storage_path' is writable.")
|
|
580
|
+
except Exception:
|
|
581
|
+
pass
|
|
582
|
+
return 1
|
|
583
|
+
sent = submit_support_bundle_email(zip_path=zip_path, include_traceback=False)
|
|
584
|
+
if not sent:
|
|
585
|
+
try:
|
|
586
|
+
print("TEST error report was not sent. See messages above for the reason. The ZIP remains queued if creation succeeded.")
|
|
587
|
+
except Exception:
|
|
588
|
+
pass
|
|
589
|
+
return 0 if sent else 1
|
|
590
|
+
except Exception as e:
|
|
591
|
+
try:
|
|
592
|
+
mc_log("[ERROR] Exception during test email report flow: {0}".format(e), level="ERROR")
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
try:
|
|
596
|
+
print("Unexpected error during TEST report flow: {0}".format(e))
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
return 1
|
|
600
|
+
|
|
601
|
+
if __name__ == "__main__":
|
|
602
|
+
raise SystemExit(email_error_report_flow())
|
|
603
|
+
|
|
@@ -594,6 +594,71 @@ def clear_token_cache():
|
|
|
594
594
|
def check_invalid_grant_causes(auth_code):
|
|
595
595
|
log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
|
|
596
596
|
|
|
597
|
+
|
|
598
|
+
def ensure_authenticated_for_gmail_send(max_wait_seconds=120):
|
|
599
|
+
"""Ensure a valid Gmail access token is available for sending.
|
|
600
|
+
|
|
601
|
+
- Reuses existing OAuth helpers in this module.
|
|
602
|
+
- Starts the local HTTPS server if needed, opens the browser for consent,
|
|
603
|
+
and polls for a token for up to max_wait_seconds.
|
|
604
|
+
- Returns True if a usable access token is available after the flow; otherwise False.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
token = get_access_token()
|
|
608
|
+
except Exception:
|
|
609
|
+
token = None
|
|
610
|
+
if token:
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
# Prepare server and certificates
|
|
614
|
+
try:
|
|
615
|
+
generate_self_signed_cert(cert_file, key_file)
|
|
616
|
+
except Exception as e:
|
|
617
|
+
log("Warning: could not ensure self-signed certs: {}".format(e))
|
|
618
|
+
|
|
619
|
+
server_started_here = False
|
|
620
|
+
global httpd
|
|
621
|
+
try:
|
|
622
|
+
if httpd is None:
|
|
623
|
+
log("Starting local HTTPS server for OAuth redirect handling.")
|
|
624
|
+
server_thread = Thread(target=run_server)
|
|
625
|
+
server_thread.daemon = True
|
|
626
|
+
server_thread.start()
|
|
627
|
+
server_started_here = True
|
|
628
|
+
time.sleep(0.5)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
log("Failed to start OAuth local server: {}".format(e))
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
auth_url = get_authorization_url()
|
|
634
|
+
print("Opening browser to authorize Gmail permission for sending...")
|
|
635
|
+
open_browser_with_executable(auth_url)
|
|
636
|
+
except Exception as e:
|
|
637
|
+
log("Failed to open authorization URL: {}".format(e))
|
|
638
|
+
|
|
639
|
+
# Poll for token availability within timeout
|
|
640
|
+
start_ts = time.time()
|
|
641
|
+
token = None
|
|
642
|
+
while time.time() - start_ts < max_wait_seconds:
|
|
643
|
+
try:
|
|
644
|
+
token = get_access_token()
|
|
645
|
+
except Exception:
|
|
646
|
+
token = None
|
|
647
|
+
if token:
|
|
648
|
+
break
|
|
649
|
+
time.sleep(3)
|
|
650
|
+
|
|
651
|
+
if server_started_here:
|
|
652
|
+
try:
|
|
653
|
+
stop_server()
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
if not token:
|
|
658
|
+
print("Gmail authorization not completed within timeout. Please finish consent and retry.")
|
|
659
|
+
|
|
660
|
+
return bool(token)
|
|
661
|
+
|
|
597
662
|
if __name__ == "__main__":
|
|
598
663
|
signal.signal(signal.SIGINT, signal_handler)
|
|
599
664
|
signal.signal(signal.SIGTERM, signal_handler)
|