Skip to content

Commit 518b05c

Browse files
Merge pull request #748 from marauder37/feature/custom-subject
Add per-user customisable email subject for "send to eReader" emails
2 parents f3a507e + f7b28d4 commit 518b05c

11 files changed

Lines changed: 179 additions & 143 deletions

File tree

CONTRIBUTORS

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,16 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
303303
- zhiyue (1 commits)
304304
# Fork Contributors (crocodilestick/calibre-web-automated)
305305

306-
- crocodilestick (762 commits)
306+
- crocodilestick (772 commits)
307307
- jmarmstrong1207 (73 commits)
308308
- demitrix (30 commits)
309309
- sirwolfgang (22 commits)
310310
- Domoel (21 commits)
311311
- wolffshots (14 commits)
312312
- angelicadvocate (9 commits)
313+
- jgoguen (9 commits)
313314
- natabat (8 commits)
314315
- nstwfdev (8 commits)
315-
- jgoguen (7 commits)
316316
- alva-seal (6 commits)
317317
- smevawala (5 commits)
318318
- Aymendje (4 commits)
@@ -328,9 +328,11 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
328328
- sethvoltz (2 commits)
329329
- Strubbl (2 commits)
330330
- tecosaur (2 commits)
331+
- TexGG (2 commits)
331332
- tseho (2 commits)
332333
- Valenth (2 commits)
333334
- a-eukarya (1 commits)
335+
- AlexSat (1 commits)
334336
- Andrej Kralj (anon) (1 commits)
335337
- bcrdncola (1 commits)
336338
- brunofin (1 commits)
@@ -354,6 +356,7 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
354356
- jack1lee1995 (anon) (1 commits)
355357
- jspiers (1 commits)
356358
- kevpam (1 commits)
359+
- lazyusername (1 commits)
357360
- Marodeur80 (anon) (1 commits)
358361
- Matteo Benaroyo (anon) (1 commits)
359362
- morpheus65535 (1 commits)

cps/admin.py

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -205,30 +205,30 @@ def reconnect():
205205
def update_thumbnails():
206206
# Always allow manual thumbnail cache updates
207207
log.info("Update of Cover cache requested")
208-
208+
209209
try:
210210
from .tasks.thumbnail import TaskGenerateCoverThumbnails
211211
task_id = helper.update_thumbnail_cache()
212-
212+
213213
# Check if there are any books to process
214214
books_with_covers = TaskGenerateCoverThumbnails.get_books_with_covers()
215215
book_count = len(books_with_covers)
216-
216+
217217
if book_count > 0:
218218
message = _('Thumbnail cache refresh started for {} book(s). This may take a few minutes.').format(book_count)
219219
else:
220220
message = _('No books with covers found to process.')
221-
221+
222222
return jsonify({
223-
'success': True,
223+
'success': True,
224224
'message': message,
225225
'book_count': book_count,
226226
'task_id': str(task_id) if task_id else None
227227
})
228228
except Exception as e:
229229
log.error(f"Error starting thumbnail refresh: {e}")
230230
return jsonify({
231-
'success': False,
231+
'success': False,
232232
'message': _('Failed to start thumbnail refresh: {}').format(str(e))
233233
})
234234

@@ -543,6 +543,8 @@ def edit_list_user(param):
543543
user.kobo_only_shelves_sync = int(vals['value'] == 'true')
544544
elif param == 'kindle_mail':
545545
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
546+
elif param == 'kindle_mail_subject':
547+
user.kindle_mail_subject = vals['value']
546548
elif param.endswith('role'):
547549
value = int(vals['field_index'])
548550
if user.name == "Guest" and value in \
@@ -1215,13 +1217,13 @@ def _configuration_oauth_helper(to_save):
12151217
if to_save["config_generic_oauth_client_secret"] != element["oauth_client_secret"]:
12161218
reboot_required = True
12171219
update["oauth_client_secret"] = to_save["config_generic_oauth_client_secret"]
1218-
1220+
12191221
# Handle metadata URL (takes precedence over manual configuration)
12201222
metadata_url = to_save.get("config_generic_oauth_metadata_url", "")
12211223
if metadata_url != element.get("metadata_url", ""):
12221224
reboot_required = True
12231225
update["metadata_url"] = metadata_url
1224-
1226+
12251227
# If metadata URL is provided, try to fetch endpoints
12261228
if metadata_url:
12271229
try:
@@ -1240,7 +1242,7 @@ def _configuration_oauth_helper(to_save):
12401242
log.warning(f"Failed to fetch OAuth metadata: {ex}")
12411243
except Exception as ex:
12421244
log.error(f"Unexpected error fetching OAuth metadata: {ex}")
1243-
1245+
12441246
# Handle manual server URL (fallback or override)
12451247
elif to_save["config_generic_oauth_server_url"] != element["oauth_base_url"]:
12461248
reboot_required = True
@@ -1264,41 +1266,41 @@ def _configuration_oauth_helper(to_save):
12641266
log.warning(f"Failed to fetch OIDC configuration: {ex}")
12651267
except Exception as ex:
12661268
log.error(f"Unexpected error fetching OIDC configuration: {ex}")
1267-
1269+
12681270
# Handle manual endpoint URLs if metadata URL is not used
12691271
if not metadata_url:
12701272
# Map form field names to database field names
12711273
endpoint_mappings = {
12721274
"config_generic_oauth_auth_url": "oauth_authorize_url",
1273-
"config_generic_oauth_token_url": "oauth_token_url",
1275+
"config_generic_oauth_token_url": "oauth_token_url",
12741276
"config_generic_oauth_userinfo_url": "oauth_userinfo_url"
12751277
}
1276-
1278+
12771279
for form_field, db_field in endpoint_mappings.items():
12781280
if form_field in to_save and to_save[form_field] != element.get(db_field, ""):
12791281
reboot_required = True
12801282
update[db_field] = to_save[form_field]
1281-
1283+
12821284
# Handle scope
12831285
if to_save.get("config_generic_oauth_scope", "") != element.get("scope", ""):
12841286
reboot_required = True
12851287
update["scope"] = to_save.get("config_generic_oauth_scope", "")
1286-
1288+
12871289
# Handle username mapper
12881290
if to_save.get("config_generic_oauth_username_mapper", "") != element.get("username_mapper", ""):
12891291
reboot_required = True
12901292
update["username_mapper"] = to_save.get("config_generic_oauth_username_mapper", "")
1291-
1293+
12921294
# Handle email mapper
12931295
if to_save.get("config_generic_oauth_email_mapper", "") != element.get("email_mapper", ""):
12941296
reboot_required = True
12951297
update["email_mapper"] = to_save.get("config_generic_oauth_email_mapper", "")
1296-
1298+
12971299
# Handle login button text
12981300
if to_save.get("config_generic_oauth_login_button", "") != element.get("login_button", ""):
12991301
reboot_required = True
13001302
update["login_button"] = to_save.get("config_generic_oauth_login_button", "")
1301-
1303+
13021304
if to_save["config_generic_oauth_admin_group"] != element["oauth_admin_group"]:
13031305
reboot_required = True
13041306
update["oauth_admin_group"] = to_save["config_generic_oauth_admin_group"]
@@ -1993,18 +1995,18 @@ def _configuration_update_helper():
19931995
_config_checkbox(to_save, "config_hardcover_sync")
19941996
_config_checkbox(to_save, "config_hardcover_annotations_sync")
19951997
_config_string(to_save, "config_hardcover_token")
1996-
1998+
19971999
_config_int(to_save, "config_updatechannel")
19982000

19992001
# Reverse proxy login configuration
20002002
_config_checkbox(to_save, "config_allow_reverse_proxy_header_login")
20012003
_config_string(to_save, "config_reverse_proxy_login_header_name")
20022004
_config_checkbox(to_save, "config_reverse_proxy_auto_create_users")
2003-
2005+
20042006
# Validate reverse proxy configuration
20052007
if config.config_reverse_proxy_auto_create_users and not config.config_allow_reverse_proxy_header_login:
20062008
return _configuration_result(_('Auto-create users cannot be enabled without enabling reverse proxy authentication'))
2007-
2009+
20082010
if config.config_reverse_proxy_auto_create_users and not config.config_reverse_proxy_login_header_name:
20092011
return _configuration_result(_('Auto-create users requires a valid reverse proxy header name'))
20102012

@@ -2013,32 +2015,32 @@ def _configuration_update_helper():
20132015
if "config_oauth_redirect_host" in to_save:
20142016
old_host = getattr(config, 'config_oauth_redirect_host', '')
20152017
new_host = to_save["config_oauth_redirect_host"].strip()
2016-
2018+
20172019
# Validate OAuth redirect host format if provided
20182020
if new_host:
20192021
try:
20202022
# Add https:// if no scheme is provided
20212023
if not new_host.startswith(('http://', 'https://')):
20222024
new_host = f"https://{new_host}"
20232025
to_save["config_oauth_redirect_host"] = new_host
2024-
2026+
20252027
# Parse the URL to validate it
20262028
parsed = urlparse(new_host)
20272029
if not parsed.netloc:
20282030
return _configuration_result(_('Invalid OAuth Redirect Host format. Please include the full URL with protocol (e.g., https://your-domain.com)'))
2029-
2031+
20302032
# Warn if URL contains a path (could cause redirect URI issues)
20312033
if parsed.path and parsed.path != '/':
20322034
return _configuration_result(_('OAuth Redirect Host should not include a path. Use only the base URL (e.g., https://your-domain.com)'))
2033-
2035+
20342036
except Exception:
20352037
return _configuration_result(_('Invalid OAuth Redirect Host format. Please include the full URL with protocol (e.g., https://your-domain.com)'))
2036-
2038+
20372039
if old_host != new_host:
20382040
oauth_redirect_host_changed = True
2039-
2041+
20402042
_config_string(to_save, "config_oauth_redirect_host")
2041-
2043+
20422044
if config.config_login_type == constants.LOGIN_OAUTH:
20432045
reboot, message = _configuration_oauth_helper(to_save)
20442046
if message:
@@ -2295,6 +2297,9 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
22952297
content.name = check_username(to_save["name"])
22962298
if to_save.get("kindle_mail") != content.kindle_mail:
22972299
content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else ""
2300+
if to_save.get("kindle_mail_subject") is not None:
2301+
content.kindle_mail_subject = (to_save.get("kindle_mail_subject", "") or "").strip()
2302+
22982303
except Exception as ex:
22992304
log.error(ex)
23002305
flash(str(ex), category="error")
@@ -2324,7 +2329,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
23242329

23252330

23262331
def extract_user_data_from_field(user, field):
2327-
match = re.search(field + r"=(.*?)($|(?<!\\),)", user, re.IGNORECASE | re.UNICODE)
2332+
match = re.search(field + r"=(.*?)($|(?<!\\),)", user, re.IGNORECASE | re.UNICODE)
23282333
if match:
23292334
return match.group(1)
23302335
else:
@@ -2362,7 +2367,7 @@ def test_oidc():
23622367
response.raise_for_status()
23632368
# Try to parse the JSON and extract useful information
23642369
oidc_config = response.json()
2365-
2370+
23662371
# Extract key endpoints for validation
23672372
endpoints = []
23682373
if 'authorization_endpoint' in oidc_config:
@@ -2371,12 +2376,12 @@ def test_oidc():
23712376
endpoints.append('token')
23722377
if 'userinfo_endpoint' in oidc_config:
23732378
endpoints.append('userinfo')
2374-
2379+
23752380
endpoint_info = " Found endpoints: " + ', '.join(endpoints) + "." if endpoints else ""
2376-
2381+
23772382
return json.dumps({
2378-
'success': True,
2379-
'message': _('Connection successful! OIDC discovery endpoint is accessible.%(endpoints)s',
2383+
'success': True,
2384+
'message': _('Connection successful! OIDC discovery endpoint is accessible.%(endpoints)s',
23802385
endpoints=endpoint_info)
23812386
})
23822387
except requests.exceptions.HTTPError as e:
@@ -2410,31 +2415,31 @@ def test_metadata():
24102415
response = requests.get(metadata_url, timeout=5, verify=constants.OAUTH_SSL_STRICT)
24112416
response.raise_for_status()
24122417
data = response.json()
2413-
2418+
24142419
# Validate that it contains required OIDC fields
24152420
required_fields = ['issuer', 'authorization_endpoint', 'token_endpoint']
24162421
missing_fields = [field for field in required_fields if not data.get(field)]
2417-
2422+
24182423
if missing_fields:
24192424
return json.dumps({
2420-
'success': False,
2421-
'message': _('Metadata is missing required OIDC fields: %(fields)s. This may not be a valid OIDC metadata endpoint.',
2425+
'success': False,
2426+
'message': _('Metadata is missing required OIDC fields: %(fields)s. This may not be a valid OIDC metadata endpoint.',
24222427
fields=', '.join(missing_fields))
24232428
}), 200
2424-
2429+
24252430
# Count available OAuth endpoints for user feedback
2426-
oauth_endpoints = ['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint',
2431+
oauth_endpoints = ['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint',
24272432
'end_session_endpoint', 'introspection_endpoint', 'revocation_endpoint']
24282433
found_endpoints = [ep for ep in oauth_endpoints if ep in data]
24292434
endpoint_count = len(found_endpoints)
24302435
has_userinfo = 'userinfo_endpoint' in data
2431-
2436+
24322437
message = _('Metadata URL is valid! Found %(count)s OAuth endpoints.', count=endpoint_count)
24332438
if has_userinfo:
24342439
message += _(' User info endpoint is available.')
24352440
else:
24362441
message += _(' Note: User info endpoint not found - this may cause authentication issues.')
2437-
2442+
24382443
return json.dumps({'success': True, 'message': message})
24392444
except requests.exceptions.HTTPError as e:
24402445
if e.response.status_code == 404:

0 commit comments

Comments
 (0)