medicafe 0.251023.2__py3-none-any.whl → 0.251027.0__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.
- MediBot/MediBot.bat +867 -835
- MediBot/__init__.py +1 -1
- MediCafe/MediLink_ConfigLoader.py +8 -1
- MediCafe/__init__.py +1 -1
- MediCafe/error_reporter.py +219 -148
- MediLink/MediLink_Gmail.py +28 -4
- MediLink/MediLink_main.py +68 -47
- MediLink/__init__.py +1 -1
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/METADATA +1 -1
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/RECORD +14 -14
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/LICENSE +0 -0
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/WHEEL +0 -0
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.251023.2.dist-info → medicafe-0.251027.0.dist-info}/top_level.txt +0 -0
MediBot/__init__.py
CHANGED
|
@@ -38,7 +38,14 @@ def get_default_config():
|
|
|
38
38
|
'endpoint_url': '',
|
|
39
39
|
'auth_token': '',
|
|
40
40
|
'insecure_http': False,
|
|
41
|
-
'max_bundle_bytes': 2097152
|
|
41
|
+
'max_bundle_bytes': 2097152,
|
|
42
|
+
'email': {
|
|
43
|
+
'enabled': False,
|
|
44
|
+
'to': '',
|
|
45
|
+
'subject_prefix': 'MediCafe Error Report',
|
|
46
|
+
'max_bundle_bytes': 1572864,
|
|
47
|
+
'transport': 'gmail_api'
|
|
48
|
+
}
|
|
42
49
|
},
|
|
43
50
|
# STRATEGIC NOTE (COB Configuration): COB library is fully implemented and ready
|
|
44
51
|
# To enable COB functionality, add the following configuration:
|
MediCafe/__init__.py
CHANGED
MediCafe/error_reporter.py
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import zipfile
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
import requests
|
|
5
|
-
except Exception:
|
|
6
|
-
requests = None
|
|
7
|
-
|
|
8
|
-
from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
|
|
10
|
+
import requests
|
|
9
11
|
|
|
12
|
+
from email.mime.application import MIMEApplication
|
|
13
|
+
from email.mime.multipart import MIMEMultipart
|
|
14
|
+
from email.mime.text import MIMEText
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
("'", "'"),
|
|
14
|
-
]
|
|
16
|
+
from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
|
|
17
|
+
from MediLink.MediLink_Gmail import get_access_token
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def _safe_ascii(text):
|
|
@@ -58,21 +61,33 @@ def _get_latest_log_path(local_storage_path):
|
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
def _redact(text):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
91
|
|
|
77
92
|
|
|
78
93
|
def _ensure_dir(path):
|
|
@@ -84,18 +99,10 @@ def _ensure_dir(path):
|
|
|
84
99
|
return False
|
|
85
100
|
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
h = hashlib.sha256()
|
|
90
|
-
with open(zip_path, 'rb') as f:
|
|
91
|
-
chunk = f.read(256 * 1024)
|
|
92
|
-
h.update(chunk)
|
|
93
|
-
return 'mc-{}-{}'.format(int(time.time()), h.hexdigest()[:12])
|
|
94
|
-
except Exception:
|
|
95
|
-
return 'mc-{}-{}'.format(int(time.time()), '000000000000')
|
|
102
|
+
# _compute_report_id removed (unused)
|
|
96
103
|
|
|
97
104
|
|
|
98
|
-
def collect_support_bundle(include_traceback=True, max_log_lines=
|
|
105
|
+
def collect_support_bundle(include_traceback=True, max_log_lines=500):
|
|
99
106
|
config, _ = load_configuration()
|
|
100
107
|
medi = config.get('MediLink_Config', {})
|
|
101
108
|
local_storage_path = medi.get('local_storage_path', '.')
|
|
@@ -120,7 +127,7 @@ def collect_support_bundle(include_traceback=True, max_log_lines=2000):
|
|
|
120
127
|
except Exception:
|
|
121
128
|
traceback_txt = ''
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
meta = {
|
|
124
131
|
'app_version': _safe_ascii(_get_version()),
|
|
125
132
|
'python_version': _safe_ascii(sys.version.split(' ')[0]),
|
|
126
133
|
'platform': _safe_ascii(platform.platform()),
|
|
@@ -140,12 +147,25 @@ def collect_support_bundle(include_traceback=True, max_log_lines=2000):
|
|
|
140
147
|
z.writestr('log_tail.txt', log_tail)
|
|
141
148
|
if traceback_txt:
|
|
142
149
|
z.writestr('traceback.txt', traceback_txt)
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
# Get latest WinSCP log
|
|
151
|
+
upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
|
|
152
|
+
download_log = os.path.join(local_storage_path, 'winscp_download.log')
|
|
153
|
+
winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
|
|
154
|
+
if winscp_logs:
|
|
155
|
+
latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
|
|
156
|
+
winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
|
|
157
|
+
winscp_tail = _redact(winscp_tail)
|
|
158
|
+
else:
|
|
159
|
+
winscp_tail = ''
|
|
160
|
+
if winscp_tail:
|
|
161
|
+
z.writestr('winscp_log_tail.txt', winscp_tail)
|
|
145
162
|
except Exception as e:
|
|
146
163
|
mc_log('Error creating support bundle: {}'.format(e), level='ERROR')
|
|
147
164
|
return None
|
|
148
165
|
|
|
166
|
+
# Do not delete oversize bundles here; sender will enforce size based on config
|
|
167
|
+
return zip_path
|
|
168
|
+
|
|
149
169
|
|
|
150
170
|
def _first_line(text):
|
|
151
171
|
try:
|
|
@@ -158,14 +178,6 @@ def _first_line(text):
|
|
|
158
178
|
return ''
|
|
159
179
|
|
|
160
180
|
|
|
161
|
-
def _readme_text():
|
|
162
|
-
return (
|
|
163
|
-
"MediCafe Support Bundle\n\n"
|
|
164
|
-
"This archive contains a redacted log tail, optional traceback, and metadata.\n"
|
|
165
|
-
"You may submit this bundle automatically from the app or send it manually to support.\n"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
181
|
def _get_version():
|
|
170
182
|
try:
|
|
171
183
|
from MediCafe import __version__
|
|
@@ -174,105 +186,6 @@ def _get_version():
|
|
|
174
186
|
return 'unknown'
|
|
175
187
|
|
|
176
188
|
|
|
177
|
-
def submit_support_bundle(zip_path):
|
|
178
|
-
config, _ = load_configuration()
|
|
179
|
-
medi = config.get('MediLink_Config', {})
|
|
180
|
-
rep = medi.get('error_reporting', {}) if isinstance(medi, dict) else {}
|
|
181
|
-
endpoint_url = _safe_ascii(rep.get('endpoint_url', ''))
|
|
182
|
-
auth_token = _safe_ascii(rep.get('auth_token', ''))
|
|
183
|
-
insecure = bool(rep.get('insecure_http', False))
|
|
184
|
-
max_bytes = int(rep.get('max_bundle_bytes', 2097152))
|
|
185
|
-
|
|
186
|
-
if not requests:
|
|
187
|
-
print("[ERROR] requests module not available; cannot submit report.")
|
|
188
|
-
return False
|
|
189
|
-
if not endpoint_url:
|
|
190
|
-
print("[ERROR] error_reporting.endpoint_url not configured.")
|
|
191
|
-
return False
|
|
192
|
-
if not os.path.exists(zip_path):
|
|
193
|
-
print("[ERROR] Bundle not found: {}".format(zip_path))
|
|
194
|
-
return False
|
|
195
|
-
try:
|
|
196
|
-
size = os.path.getsize(zip_path)
|
|
197
|
-
if size > max_bytes:
|
|
198
|
-
print("[INFO] Bundle size {} exceeds cap {}; rebuilding smaller not implemented here.".format(size, max_bytes))
|
|
199
|
-
except Exception:
|
|
200
|
-
pass
|
|
201
|
-
|
|
202
|
-
report_id = _compute_report_id(zip_path)
|
|
203
|
-
headers = {
|
|
204
|
-
'X-Auth-Token': auth_token or '',
|
|
205
|
-
'X-Report-Id': report_id,
|
|
206
|
-
'User-Agent': 'MediCafe-Reporter/1.0'
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# Prepare meta.json stream derived from inside the zip for server convenience
|
|
210
|
-
meta_json = '{}'
|
|
211
|
-
try:
|
|
212
|
-
with zipfile.ZipFile(zip_path, 'r') as z:
|
|
213
|
-
if 'meta.json' in z.namelist():
|
|
214
|
-
meta_json = z.read('meta.json')
|
|
215
|
-
except Exception:
|
|
216
|
-
meta_json = '{}'
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
bundle_fh = open(zip_path, 'rb')
|
|
220
|
-
files = {
|
|
221
|
-
'meta': ('meta.json', meta_json, 'application/json'),
|
|
222
|
-
'bundle': (os.path.basename(zip_path), bundle_fh, 'application/zip')
|
|
223
|
-
}
|
|
224
|
-
r = requests.post(endpoint_url, headers=headers, files=files, timeout=(10, 20), verify=(not insecure))
|
|
225
|
-
code = getattr(r, 'status_code', None)
|
|
226
|
-
if code == 200:
|
|
227
|
-
print("[SUCCESS] Report submitted. ID: {}".format(report_id))
|
|
228
|
-
return True
|
|
229
|
-
elif code == 401:
|
|
230
|
-
print("[ERROR] Unauthorized (401). Check error_reporting.auth_token.")
|
|
231
|
-
return False
|
|
232
|
-
elif code == 403:
|
|
233
|
-
print("[ERROR] Forbidden (403). The receiver denied access. Verify REPORT_TOKEN and receiver permissions.")
|
|
234
|
-
return False
|
|
235
|
-
elif code == 413:
|
|
236
|
-
print("[ERROR] Too large (413). Consider reducing max log lines.")
|
|
237
|
-
return False
|
|
238
|
-
else:
|
|
239
|
-
print("[ERROR] Submission failed with status {}".format(code))
|
|
240
|
-
return False
|
|
241
|
-
except Exception as e:
|
|
242
|
-
print("[ERROR] Submission exception: {}".format(e))
|
|
243
|
-
return False
|
|
244
|
-
finally:
|
|
245
|
-
try:
|
|
246
|
-
bundle_fh.close()
|
|
247
|
-
except Exception:
|
|
248
|
-
pass
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def flush_queued_reports():
|
|
252
|
-
config, _ = load_configuration()
|
|
253
|
-
medi = config.get('MediLink_Config', {})
|
|
254
|
-
local_storage_path = medi.get('local_storage_path', '.')
|
|
255
|
-
queue_dir = os.path.join(local_storage_path, 'reports_queue')
|
|
256
|
-
if not os.path.isdir(queue_dir):
|
|
257
|
-
return 0, 0
|
|
258
|
-
count_ok = 0
|
|
259
|
-
count_total = 0
|
|
260
|
-
for name in sorted(os.listdir(queue_dir)):
|
|
261
|
-
if not name.endswith('.zip'):
|
|
262
|
-
continue
|
|
263
|
-
zip_path = os.path.join(queue_dir, name)
|
|
264
|
-
count_total += 1
|
|
265
|
-
print("Attempting upload of queued report: {}".format(name))
|
|
266
|
-
ok = submit_support_bundle(zip_path)
|
|
267
|
-
if ok:
|
|
268
|
-
try:
|
|
269
|
-
os.remove(zip_path)
|
|
270
|
-
except Exception:
|
|
271
|
-
pass
|
|
272
|
-
count_ok += 1
|
|
273
|
-
return count_ok, count_total
|
|
274
|
-
|
|
275
|
-
|
|
276
189
|
def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
|
|
277
190
|
try:
|
|
278
191
|
config, _ = load_configuration()
|
|
@@ -284,7 +197,165 @@ def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
|
|
|
284
197
|
text = _redact(text)
|
|
285
198
|
with open(trace_path, 'w') as f:
|
|
286
199
|
f.write(text)
|
|
287
|
-
|
|
200
|
+
print("An error occurred. A traceback was saved to {}".format(trace_path))
|
|
288
201
|
except Exception:
|
|
289
|
-
|
|
202
|
+
try:
|
|
203
|
+
mc_log('Failed to capture traceback to file', level='WARNING')
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
def submit_support_bundle_email(zip_path=None, include_traceback=True):
|
|
208
|
+
if not zip_path:
|
|
209
|
+
zip_path = collect_support_bundle(include_traceback)
|
|
210
|
+
if not zip_path:
|
|
211
|
+
mc_log("Failed to create bundle.", level="ERROR")
|
|
212
|
+
return False
|
|
213
|
+
bundle_size = os.path.getsize(zip_path)
|
|
214
|
+
config, _ = load_configuration()
|
|
215
|
+
email_config = config.get('MediLink_Config', {}).get('error_reporting', {}).get('email', {})
|
|
216
|
+
# Determine max size from config with default 1.5MB
|
|
217
|
+
try:
|
|
218
|
+
max_bytes = int(email_config.get('max_bundle_bytes', 1572864))
|
|
219
|
+
except Exception:
|
|
220
|
+
max_bytes = 1572864
|
|
221
|
+
if bundle_size > max_bytes:
|
|
222
|
+
mc_log("Bundle too large ({} bytes > {} bytes) - leaving in queue.".format(bundle_size, max_bytes), level="WARNING")
|
|
223
|
+
return False
|
|
224
|
+
# Feature is always available; proceed if recipients and token are available
|
|
225
|
+
# Normalize and validate recipients
|
|
226
|
+
to_emails = _normalize_recipients(email_config.get('to', []))
|
|
227
|
+
if not to_emails:
|
|
228
|
+
mc_log("No valid recipients configured in error_reporting.email.to", level="ERROR")
|
|
229
|
+
return False
|
|
230
|
+
subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
|
|
231
|
+
access_token = get_access_token()
|
|
232
|
+
if not access_token:
|
|
233
|
+
mc_log("No access token - authenticate first.", level="ERROR")
|
|
234
|
+
return False
|
|
235
|
+
mc_log("Building email...", level="INFO")
|
|
236
|
+
msg = MIMEMultipart()
|
|
237
|
+
msg['To'] = ', '.join(to_emails)
|
|
238
|
+
msg['Subject'] = '{} - {}'.format(subject_prefix, time.strftime('%Y%m%d_%H%M%S'))
|
|
239
|
+
with open(zip_path, 'rb') as f:
|
|
240
|
+
attach = MIMEApplication(f.read(), _subtype='zip')
|
|
241
|
+
attach.add_header('Content-Disposition', 'attachment', filename=os.path.basename(zip_path))
|
|
242
|
+
msg.attach(attach)
|
|
243
|
+
body = "Error report attached."
|
|
244
|
+
msg.attach(MIMEText(body, 'plain'))
|
|
245
|
+
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
|
|
246
|
+
mc_log("Sending report...", level="INFO")
|
|
247
|
+
headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
|
|
248
|
+
data = {'raw': raw}
|
|
249
|
+
resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
|
|
250
|
+
if resp.status_code == 200:
|
|
251
|
+
mc_log("Report sent successfully!", level="INFO")
|
|
252
|
+
try:
|
|
253
|
+
os.remove(zip_path)
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
return True
|
|
257
|
+
else:
|
|
258
|
+
# Handle auth errors by prompting re-consent using existing OAuth helpers
|
|
259
|
+
if resp.status_code in (401, 403):
|
|
260
|
+
try:
|
|
261
|
+
from MediLink.MediLink_Gmail import get_authorization_url, open_browser_with_executable
|
|
262
|
+
auth_url = get_authorization_url()
|
|
263
|
+
mc_log("Gmail send unauthorized ({}). Opening browser for re-consent.".format(resp.status_code), level="WARNING")
|
|
264
|
+
try:
|
|
265
|
+
print("Your Google session needs to be refreshed to gain gmail.send permission. A browser will open to re-authorize.")
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
open_browser_with_executable(auth_url)
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
mc_log("Failed to send: {} - {}".format(resp.status_code, _redact(resp.text)), level="ERROR")
|
|
272
|
+
# Preserve bundle in queue for manual retry
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _normalize_recipients(to_field):
|
|
277
|
+
try:
|
|
278
|
+
# Flatten to a list of strings
|
|
279
|
+
if isinstance(to_field, str):
|
|
280
|
+
candidates = [p.strip() for p in to_field.split(',')]
|
|
281
|
+
elif isinstance(to_field, list):
|
|
282
|
+
candidates = [str(p).strip() for p in to_field]
|
|
283
|
+
else:
|
|
284
|
+
candidates = []
|
|
285
|
+
# Basic email regex: local@domain.tld
|
|
286
|
+
import re
|
|
287
|
+
email_re = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
|
288
|
+
valid = []
|
|
289
|
+
for addr in candidates:
|
|
290
|
+
if not addr:
|
|
291
|
+
continue
|
|
292
|
+
if email_re.match(addr):
|
|
293
|
+
valid.append(addr)
|
|
294
|
+
else:
|
|
295
|
+
try:
|
|
296
|
+
mc_log("Invalid email recipient skipped: {}".format(addr), level="WARNING")
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
return valid
|
|
300
|
+
except Exception:
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def list_queued_bundles():
|
|
305
|
+
try:
|
|
306
|
+
config, _ = load_configuration()
|
|
307
|
+
medi = config.get('MediLink_Config', {})
|
|
308
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
309
|
+
queue_dir = os.path.join(local_storage_path, 'reports_queue')
|
|
310
|
+
if not os.path.isdir(queue_dir):
|
|
311
|
+
return []
|
|
312
|
+
files = [os.path.join(queue_dir, f) for f in os.listdir(queue_dir) if f.endswith('.zip')]
|
|
313
|
+
files.sort()
|
|
314
|
+
return files
|
|
315
|
+
except Exception:
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def submit_all_queued_bundles():
|
|
320
|
+
sent = 0
|
|
321
|
+
failed = 0
|
|
322
|
+
try:
|
|
323
|
+
queued = list_queued_bundles()
|
|
324
|
+
for z in queued:
|
|
325
|
+
try:
|
|
326
|
+
ok = submit_support_bundle_email(zip_path=z, include_traceback=False)
|
|
327
|
+
if ok:
|
|
328
|
+
sent += 1
|
|
329
|
+
else:
|
|
330
|
+
failed += 1
|
|
331
|
+
except Exception:
|
|
332
|
+
failed += 1
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
return sent, failed
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def delete_all_queued_bundles():
|
|
339
|
+
deleted = 0
|
|
340
|
+
try:
|
|
341
|
+
for z in list_queued_bundles():
|
|
342
|
+
try:
|
|
343
|
+
os.remove(z)
|
|
344
|
+
deleted += 1
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
return deleted
|
|
350
|
+
|
|
351
|
+
def email_error_report_flow():
|
|
352
|
+
try:
|
|
353
|
+
sent = submit_support_bundle_email(zip_path=None, include_traceback=True)
|
|
354
|
+
return 0 if sent else 1
|
|
355
|
+
except Exception as e:
|
|
356
|
+
mc_log("[ERROR] Exception during email report flow: {0}".format(e), level="ERROR")
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
raise SystemExit(email_error_report_flow())
|
|
290
361
|
|
MediLink/MediLink_Gmail.py
CHANGED
|
@@ -84,6 +84,7 @@ SCOPES = ' '.join([
|
|
|
84
84
|
'https://www.googleapis.com/auth/gmail.modify',
|
|
85
85
|
'https://www.googleapis.com/auth/gmail.compose',
|
|
86
86
|
'https://www.googleapis.com/auth/gmail.readonly',
|
|
87
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
87
88
|
'https://www.googleapis.com/auth/script.external_request',
|
|
88
89
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
89
90
|
'https://www.googleapis.com/auth/script.scriptapp',
|
|
@@ -114,11 +115,34 @@ def get_authorization_url():
|
|
|
114
115
|
def exchange_code_for_token(auth_code, retries=3):
|
|
115
116
|
return oauth_exchange_code_for_token(auth_code, CREDENTIALS_PATH, REDIRECT_URI, log, retries=retries)
|
|
116
117
|
|
|
118
|
+
def _mask_token_value(value):
|
|
119
|
+
try:
|
|
120
|
+
s = str(value or '')
|
|
121
|
+
if len(s) <= 8:
|
|
122
|
+
return '***'
|
|
123
|
+
return s[:4] + '…' + s[-4:]
|
|
124
|
+
except Exception:
|
|
125
|
+
return '***'
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _mask_token_payload(token_data):
|
|
129
|
+
try:
|
|
130
|
+
if not isinstance(token_data, dict):
|
|
131
|
+
return '{"token":"***"}'
|
|
132
|
+
masked = dict(token_data)
|
|
133
|
+
for k in ('access_token', 'refresh_token', 'id_token'):
|
|
134
|
+
if k in masked:
|
|
135
|
+
masked[k] = _mask_token_value(masked.get(k))
|
|
136
|
+
return masked
|
|
137
|
+
except Exception:
|
|
138
|
+
return '{"token":"***"}'
|
|
139
|
+
|
|
140
|
+
|
|
117
141
|
def get_access_token():
|
|
118
142
|
if os.path.exists(TOKEN_PATH):
|
|
119
143
|
with open(TOKEN_PATH, 'r') as token_file:
|
|
120
144
|
token_data = json.load(token_file)
|
|
121
|
-
log("Loaded token data:\n {}".format(token_data))
|
|
145
|
+
log("Loaded token data (masked):\n {}".format(_mask_token_payload(token_data)))
|
|
122
146
|
if 'access_token' in token_data and 'expires_in' in token_data:
|
|
123
147
|
try:
|
|
124
148
|
token_time = token_data.get('token_time', time.time())
|
|
@@ -127,7 +151,7 @@ def get_access_token():
|
|
|
127
151
|
log("KeyError while accessing token data: {}".format(e))
|
|
128
152
|
return None
|
|
129
153
|
if token_expiry_time > time.time():
|
|
130
|
-
log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
|
|
154
|
+
log("Access token is still valid. Expires in {} seconds.".format(int(token_expiry_time - time.time())))
|
|
131
155
|
return token_data['access_token']
|
|
132
156
|
else:
|
|
133
157
|
log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
|
|
@@ -136,10 +160,10 @@ def get_access_token():
|
|
|
136
160
|
new_token_data['token_time'] = time.time()
|
|
137
161
|
with open(TOKEN_PATH, 'w') as token_file:
|
|
138
162
|
json.dump(new_token_data, token_file)
|
|
139
|
-
log("Access token refreshed successfully. New token data: {}".format(new_token_data))
|
|
163
|
+
log("Access token refreshed successfully. New token data (masked): {}".format(_mask_token_payload(new_token_data)))
|
|
140
164
|
return new_token_data['access_token']
|
|
141
165
|
else:
|
|
142
|
-
log("Failed to refresh access token.
|
|
166
|
+
log("Failed to refresh access token. Response (masked): {}".format(_mask_token_payload(new_token_data)))
|
|
143
167
|
return None
|
|
144
168
|
log("Access token not found. Please authenticate.")
|
|
145
169
|
return None
|