v3.1.1 - RC - more testing needed - Details in Changelog.md
Some checks failed
Docker Image - Webmail / build_self_hosted (push) Waiting to run
Docker Image - Webmail / build_github_hosted_fallback (push) Blocked by required conditions
Docker Image - DockFlare / build_self_hosted (push) Has been cancelled
Docker Image - Mail Manager / build_self_hosted (push) Has been cancelled
Docker Image - DockFlare / build_github_hosted_fallback (push) Has been cancelled
Docker Image - Mail Manager / build_github_hosted_fallback (push) Has been cancelled

This commit is contained in:
ChrispyBacon-dev 2026-04-24 23:32:33 +02:00
parent b3c0da2d75
commit 0fad7c592f
65 changed files with 5980 additions and 1619 deletions

View file

@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
## [v3.1.1] - 2026-04-24
### Added
- **Email Alias System:** A disposable email alias layer for the DockFlare Email Suite.
- **Alias Creation:** Generate aliases with three styles (`word-word-num`, `word-num`, `uuid-short`) or define a custom local-part. Each alias is bound to a mailbox and routed transparently.
- **Inbound Forwarding:** Aliases are enforced at the Cloudflare Worker layer via KV lookups mail to an unknown alias is rejected at the SMTP level before reaching the server. Quota enforcement follows the resolved mailbox.
- **Outbound Reply Support:** When replying to an email received via an alias, the webmail compose view pre-selects the alias as the sender. A From dropdown appears whenever aliases are configured on the active mailbox.
- **Alias Management UI:** Full alias management from the webmail Settings panel create, toggle active/inactive, set an optional expiry date, add a label and description, and delete. Usage count and last-use timestamp are tracked per alias.
- **Alias Expiry:** An hourly background job deactivates expired aliases and removes their KV entries from Cloudflare automatically.
- **Per-Alias Rate Limiting:** Maximum 20 alias creations per hour and 100 aliases per mailbox enforced server-side.
- **Webmail Compose Enhancements:**
- **Multi-Recipient To Field:** The compose dialog now supports multiple recipients in the To field. Addresses are entered as chips — press Enter, Tab, or comma to confirm. Paste a comma-separated list to add multiple at once.
- **Cc and Bcc Fields:** Cc and Bcc fields are hidden by default and revealed via inline buttons.
- **Sent Folder Recipient Display:** Message list items in the Sent folder now show recipient addresses instead of the sender's own address.
- **Emoji Picker:** An emoji selector is available in the compose toolbar. Searchable, categorized, and lazy-loaded with no impact on initial bundle size.
- **Inline Link Popover:** The insert-link toolbar button now opens an inline popover with a URL input and Apply/Cancel actions, replacing the browser-native prompt dialog.
- **Mobile Support:** The webmail is now fully responsive for phones and small screens.
- **Single-Panel Navigation:** On mobile, the three-panel desktop layout switches to a stacked single-panel view — Folders → Message List → Message Detail — with a back button to navigate up the stack.
- **Bottom Navigation Bar:** A persistent bottom bar provides quick access to Folders, Compose, and Mail with a floating action button for compose.
- **Full-Screen Compose:** The compose dialog opens as a full-screen overlay on mobile instead of the desktop floating popup.
- **iOS Safe Area Support:** Bottom navigation respects the iOS home indicator safe area via `viewport-fit=cover`.
## [v3.1.0] - 2026-04-16
> **Cloudflare Context:** Cloudflare's Email Service entered public beta today — the same `send_email` Workers binding that powers DockFlare Mail's outbound relay is now generally available. Read the announcement: [Email for Agents](https://blog.cloudflare.com/email-for-agents/)

View file

@ -35,7 +35,7 @@ def _get_int_env(name, default, minimum=None):
return default
# --- DockFlare Version ---
APP_VERSION = "v3.1.0"
APP_VERSION = "v3.1.1"
# --- web: https://dockflare.app ---
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---

View file

@ -337,6 +337,28 @@ def create_kv_namespace(title):
json_data={'title': title})
return res.get('result', {}).get('id')
def get_or_create_kv_namespace(title):
try:
ns_id = create_kv_namespace(title)
if ns_id:
return ns_id
except Exception:
pass
page = 1
while True:
res = cf_api_request('GET', f'/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces',
params={'per_page': 100, 'page': page})
results = res.get('result') or []
for ns in results:
if ns.get('title') == title:
return ns.get('id')
info = res.get('result_info', {})
if page >= info.get('total_pages', 1):
break
page += 1
return None
def update_kv_entry(namespace_id, key, value_dict):
url = f"{config.CF_API_BASE_URL}/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces/{namespace_id}/values/{key}"
headers = {"Authorization": f"Bearer {config.CF_API_TOKEN}", "Content-Type": "text/plain"}

View file

@ -30,10 +30,11 @@ async function dispatchWebhook(env, payload) {
}
export default {
// ── Inbound email handler ──────────────────────────────────────────────────-.--...--
async email(message, env, ctx) {
try {
let resolvedMailbox = null;
const catchAllEnabled = env.CATCH_ALL_ENABLED === 'true';
if (catchAllEnabled) {
const domain = (env.DOMAIN_NAME || '').toLowerCase();
if (!message.to.toLowerCase().endsWith('@' + domain)) {
@ -43,21 +44,28 @@ export default {
} else {
const allowedRecipients = JSON.parse(env.ALLOWED_RECIPIENTS || '[]');
if (!allowedRecipients.includes(message.to)) {
let aliasRecord = null;
try {
aliasRecord = await env.QUOTA_KV.get('alias::' + message.to, 'json');
} catch (_) {}
if (!aliasRecord) {
message.setReject("Recipient not allowed");
return;
}
resolvedMailbox = aliasRecord.mailbox;
}
}
// Check quota KV before accepting — reject at SMTP level so sender gets a bounce
if (typeof env.QUOTA_KV !== 'undefined') {
try {
const state = await env.QUOTA_KV.get(message.to, "json");
const quotaTarget = resolvedMailbox || message.to;
const state = await env.QUOTA_KV.get(quotaTarget, "json");
if (state?.blocked) {
message.setReject("550 5.2.2 Mailbox full");
return;
}
} catch (kvErr) {
// KV unavailable — fall through, webhook safety net handles enforcement
console.warn(`KV quota check failed for ${message.to}: ${kvErr.message}`);
}
}
@ -66,12 +74,13 @@ export default {
const r2Key = `temp_cache/${messageId}.eml`;
const receivedAt = new Date().toISOString();
// Upload to R2 first — email is now safely buffered regardless of what happens next
const rawBytes = await new Response(message.raw).arrayBuffer();
await env.EMAIL_BUCKET.put(r2Key, rawBytes, {
customMetadata: {
from: message.from,
to: message.to,
resolved_mailbox: resolvedMailbox || message.to,
via_alias: resolvedMailbox ? "1" : "0",
subject: message.headers.get("subject") || "",
receivedAt: receivedAt
}
@ -81,6 +90,8 @@ export default {
message_id: messageId,
from: message.from,
to: message.to,
resolved_mailbox: resolvedMailbox || message.to,
via_alias: !!resolvedMailbox,
subject: message.headers.get("subject") || "",
received_at: receivedAt,
r2_key: r2Key,
@ -92,31 +103,21 @@ export default {
if (webhookResponse.ok) {
const body = await webhookResponse.json().catch(() => ({}));
if (body.reason === 'over_hard_quota') {
// Mail Manager rejected the email (hard quota exceeded) and already cleaned
// up R2 + the DB entry. Reject at SMTP level so sender gets an NDR bounce.
message.setReject("550 5.2.2 Mailbox full");
return;
}
// On success the mail-manager deletes the R2 file itself after processing.
} else {
// DockFlare returned an error — leave email in R2 for cron retry.
// The email is safely buffered and will be delivered
// automatically when DockFlare is healthy again.
console.warn(`Webhook returned ${webhookResponse.status} for ${messageId} — buffered in R2 for retry`);
}
} catch (webhookErr) {
// DockFlare is unreachable (offline, timeout, network error).
// Email is already in R2. Cron will retry. Do NOT reject.
console.warn(`Webhook unreachable for ${messageId} — buffered in R2 for retry: ${webhookErr.message}`);
}
} catch (err) {
// Only reject if failed to store the email in R2 (truly unrecoverable). - reminder need some tests still
message.setReject(`Worker error: ${err.message}`);
}
},
// ── Cron trigger: retry buffered emails in R2 ─────────────────────────-..-.-.-.-────
async scheduled(event, env, ctx) {
console.log("Cron: scanning R2 temp_cache for buffered emails...");
@ -140,6 +141,8 @@ export default {
message_id: messageId,
from: meta.from || "",
to: meta.to || "",
resolved_mailbox: meta.resolved_mailbox || meta.to || "",
via_alias: meta.via_alias === "1",
subject: meta.subject || "",
received_at: meta.receivedAt || new Date().toISOString(),
r2_key: r2Key,
@ -151,8 +154,6 @@ export default {
if (response.ok) {
const body = await response.json().catch(() => ({}));
if (body.reason === 'over_hard_quota') {
// Mailbox was full when cron retried — webhook already cleaned R2 + set KV block.
// Count as processed (not a retry-able failure).
console.warn(`Cron: buffered email ${messageId} rejected (over_hard_quota) — R2 cleaned by Mail Manager`);
processed++;
} else {
@ -165,7 +166,6 @@ export default {
failed++;
}
} catch (err) {
// DockFlare still offline — will retry on next cron run
console.warn(`Cron: DockFlare still unreachable for ${messageId}: ${err.message}`);
failed++;
}

View file

@ -10,10 +10,8 @@ export default {
return new Response("Unauthorized", { status: 401 });
}
const body = await request.json();
const toHeader = Array.isArray(body.to) ? body.to.join(", ") : body.to;
const rawTo = Array.isArray(body.to) ? body.to[0] : body.to;
const addrMatch = typeof rawTo === 'string' ? rawTo.match(/<([^>]+)>/) : null;
const toAddress = addrMatch ? addrMatch[1] : (typeof rawTo === 'string' ? rawTo.trim() : rawTo);
const toList = Array.isArray(body.to) ? body.to : [body.to];
const toHeader = toList.join(", ");
const attachments = Array.isArray(body.attachments) ? body.attachments.filter(a => a && a.data_b64) : [];
const hasAttachments = attachments.length > 0;
@ -73,9 +71,13 @@ export default {
mimeMessage += `--${innerBoundary}--\r\n`;
}
const message = new EmailMessage(body.from, toAddress, mimeMessage);
try {
for (const recipient of toList) {
const addrMatch = typeof recipient === 'string' ? recipient.match(/<([^>]+)>/) : null;
const toAddress = addrMatch ? addrMatch[1] : (typeof recipient === 'string' ? recipient.trim() : recipient);
const message = new EmailMessage(body.from, toAddress, mimeMessage);
await env.SEND_EMAIL.send(message);
}
return new Response(JSON.stringify({ success: true, message_id: body.messageId }), {
status: 200,
headers: { "Content-Type": "application/json" }

View file

@ -399,15 +399,15 @@ def _redeploy_inbound_worker(email_cfg, domain):
kv_ns_id = d.get('quota_kv_namespace_id')
if not kv_ns_id:
try:
kv_ns_id = email_manager.create_kv_namespace(
kv_ns_id = email_manager.get_or_create_kv_namespace(
f"dockflare-quota-{domain.replace('.', '-')}"
)
if kv_ns_id:
email_cfg['domains'][domain]['quota_kv_namespace_id'] = kv_ns_id
save_email_config(email_cfg)
logging.info(f"Created quota KV namespace for existing domain {domain}: {kv_ns_id}")
except Exception as e:
logging.warning(f"Could not create quota KV namespace for {domain}: {e}")
logging.info(f"Resolved quota KV namespace for {domain}: {kv_ns_id}")
else:
logging.warning(f"Could not resolve quota KV namespace for {domain}")
catch_all_address = d.get('catch_all_address', '')
catch_all_enabled = 'true' if catch_all_address else 'false'
@ -1190,6 +1190,78 @@ def quota_kv_sync():
return jsonify({'status': 'ok', 'action': action})
@email_bp.route('/internal/alias-kv-sync', methods=['POST'])
def alias_kv_sync():
if not _check_internal_request():
return jsonify({'error': 'forbidden'}), 403
data = request.get_json(force=True, silent=True) or {}
domain = data.get('domain')
alias_address = data.get('alias_address')
action = data.get('action')
mailbox_address = data.get('mailbox_address')
if not domain or not alias_address or action not in ('put', 'delete'):
return jsonify({'error': 'missing domain, alias_address, or valid action'}), 400
if action == 'put' and not mailbox_address:
return jsonify({'error': 'mailbox_address required for put'}), 400
cfg = config.EMAIL_CONFIG
if not cfg or domain not in cfg.get('domains', {}):
return jsonify({'status': 'domain_not_found'}), 200
kv_ns_id = cfg['domains'][domain].get('quota_kv_namespace_id')
if not kv_ns_id:
return jsonify({'status': 'no_kv'}), 200
kv_key = f"alias::{alias_address}"
try:
if action == 'put':
email_manager.update_kv_entry(kv_ns_id, kv_key, {"mailbox": mailbox_address})
else:
email_manager.delete_kv_entry(kv_ns_id, kv_key)
except Exception as e:
logging.warning(f"alias-kv-sync {action} failed for {alias_address}: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'status': 'ok', 'action': action})
@email_bp.route('/domain/<domain>/aliases', methods=['GET'])
@login_required
def domain_aliases(domain):
import requests as req_lib
token = _generate_jwt(current_user.get_id(), role='admin')
if not token:
return jsonify({'error': 'JWT configuration missing'}), 500
try:
resp = req_lib.get(
f"{config.MAIL_MANAGER_INTERNAL_URL}/api/v1/system/aliases",
headers={'Authorization': f'Bearer {token}'},
params={'domain': domain},
timeout=10,
)
if resp.ok:
return jsonify(resp.json())
return jsonify({'error': 'mail-manager unreachable'}), 502
except Exception as e:
return jsonify({'error': str(e)}), 500
@email_bp.route('/alias/<path:address>/delete', methods=['POST'])
@login_required
def admin_delete_alias(address):
import requests as req_lib
token = _generate_jwt(current_user.get_id(), role='admin')
if not token:
return jsonify({'error': 'JWT configuration missing'}), 500
try:
resp = req_lib.delete(
f"{config.MAIL_MANAGER_INTERNAL_URL}/api/v1/aliases/{address}",
headers={'Authorization': f'Bearer {token}'},
timeout=10,
)
if resp.ok:
return jsonify({'status': 'deleted'})
return jsonify({'error': resp.text[:200]}), resp.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
def _restart_mail_container():
try:
container = docker_client.containers.get('dockflare-mail-manager')

View file

@ -188,7 +188,7 @@ def gating_logic():
return
if not current_user.is_authenticated:
exempt_endpoints = ['static', 'web.ping', 'web.cloudflare_ping_route', 'setup.step_import_env', 'email.internal_mail_config', 'email.mailbox_login', 'email.quota_kv_sync']
exempt_endpoints = ['static', 'web.ping', 'web.cloudflare_ping_route', 'setup.step_import_env', 'email.internal_mail_config', 'email.mailbox_login', 'email.quota_kv_sync', 'email.alias_kv_sync']
oauth_endpoints = ['web.login_provider', 'web.auth_callback', 'web.login']
if request.endpoint and not request.endpoint.startswith('auth.') and request.endpoint not in exempt_endpoints and request.endpoint not in oauth_endpoints:
try:

View file

@ -6,7 +6,7 @@ import re
import shutil
import threading
import uuid
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
import requests as http_requests
from flask import Blueprint, request, jsonify, send_file
@ -15,6 +15,7 @@ from app.config import config
from app.core.database import get_db
from app.api.middleware import jwt_required, admin_required
from app.core.rate_limiter import limiter
from app.core.alias_words import generate_alias, validate_alias_address
log = logging.getLogger(__name__)
api_bp = Blueprint('api', __name__)
@ -749,7 +750,7 @@ def _local_deliver(db, recipient_addr, from_address, to_field, data, subject, te
log.info("Local delivery: msg_id=%s to=%s", local_msg_id, recipient_addr)
def _dispatch_send(address, data):
def _dispatch_send(address, data, effective_from=None, via_alias=None):
allowed, reason = limiter.check_rate(address)
if not allowed:
return jsonify({"error": reason}), 429
@ -760,6 +761,16 @@ def _dispatch_send(address, data):
if not to_field:
return jsonify({"error": "to is required"}), 400
cc_field = data.get('cc') or []
if isinstance(cc_field, str):
cc_field = [cc_field]
data['cc'] = cc_field
bcc_field = data.get('bcc') or []
if isinstance(bcc_field, str):
bcc_field = [bcc_field]
data['bcc'] = bcc_field
subject = data.get('subject', '')
text = data.get('text') or data.get('text_body', '')
html = data.get('html') or data.get('html_body', '')
@ -773,6 +784,7 @@ def _dispatch_send(address, data):
if total_attach > _MAX_ATTACH_BYTES:
return jsonify({"error": "attachments exceed 10 MB limit"}), 413
from_address = effective_from or address
now = datetime.now(timezone.utc).isoformat()
msg_id = f"<{uuid.uuid4()}@{address.split('@')[1]}>"
@ -807,7 +819,7 @@ def _dispatch_send(address, data):
if external_recipients:
worker_payload = {
"from": address,
"from": from_address,
"to": external_recipients,
"cc": data.get('cc'),
"bcc": data.get('bcc'),
@ -840,8 +852,8 @@ def _dispatch_send(address, data):
error_msg = 'Outbound worker not configured'
db.execute(
"INSERT INTO send_log (message_id, from_address, to_addresses, subject, sent_at, status, error_message, worker_response) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(msg_id, address, json.dumps(to_field), subject, now, status, error_msg, worker_resp),
"INSERT INTO send_log (message_id, from_address, to_addresses, subject, sent_at, status, error_message, worker_response, via_alias) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(msg_id, from_address, json.dumps(to_field), subject, now, status, error_msg, worker_resp, via_alias),
)
if status == 'sent':
@ -860,7 +872,7 @@ def _dispatch_send(address, data):
reference_ids, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 0, ?, ?, ?)
""", (
msg_id, address, sent_folder['id'], address,
msg_id, address, sent_folder['id'], from_address,
json.dumps(to_field), json.dumps(data.get('cc') or []),
subject, text, html, now,
data.get('in_reply_to') or data.get('inReplyTo', ''),
@ -891,15 +903,12 @@ def send_email(address):
if not _check_mailbox_access(address):
return jsonify({"error": "forbidden"}), 403
if request.content_type and 'multipart/form-data' in request.content_type:
data = dict(request.form)
# Flatten single-value lists produced by request.form
data = {k: v[0] if isinstance(v, list) and len(v) == 1 else v for k, v in data.items()}
data = {k: request.form.getlist(k) if len(request.form.getlist(k)) > 1 else request.form.get(k)
for k in request.form.keys()}
files = request.files.getlist('attachments')
log.debug("send_email multipart: form_keys=%s file_count=%d", list(data.keys()), len(files))
attachments = []
for f in files:
raw = f.read()
log.debug("send_email attachment: filename=%s content_type=%s size=%d", f.filename, f.content_type, len(raw))
attachments.append({
'filename': f.filename,
'content_type': f.content_type or 'application/octet-stream',
@ -909,7 +918,24 @@ def send_email(address):
data['attachments'] = attachments
else:
data = request.json or {}
return _dispatch_send(address, data)
from_override = (data.get('from_address') or '').strip()
effective_from = None
via_alias = None
if from_override and from_override != address:
db = get_db()
now_iso = datetime.now(timezone.utc).isoformat()
alias_row = db.execute(
"SELECT address FROM aliases WHERE address=? AND mailbox_address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
(from_override, address, now_iso)
).fetchone()
if not alias_row:
return jsonify({"error": "unauthorized_sender"}), 403
effective_from = from_override
via_alias = from_override
return _dispatch_send(address, data, effective_from=effective_from, via_alias=via_alias)
@api_bp.route('/mailboxes/<address>/drafts', methods=['POST'])
@ -1233,3 +1259,264 @@ def list_auto_responders():
db = get_db()
rows = db.execute("SELECT mailbox_address, is_active FROM auto_responders").fetchall()
return jsonify({"auto_responders": [dict(r) for r in rows]})
_ALIAS_MAX_PER_MAILBOX = int(os.environ.get('ALIAS_MAX_PER_MAILBOX', 100))
_ALIAS_RATE_LIMIT_PER_HOUR = 20
def _sync_alias_kv(alias_address, mailbox_address, action):
master_url = os.environ.get('DOCKFLARE_MASTER_URL', '').rstrip('/')
if not master_url:
return False
payload = {"domain": alias_address.split('@')[1], "alias_address": alias_address, "action": action}
if action == "put":
payload["mailbox_address"] = mailbox_address
try:
resp = http_requests.post(
f"{master_url}/email/internal/alias-kv-sync",
json=payload,
headers={"X-Bootstrap-Token": os.environ.get("INTERNAL_BOOTSTRAP_SECRET", "")},
timeout=5,
)
if not resp.ok:
log.error("alias-kv-sync %s failed for %s: HTTP %s %s", action, alias_address, resp.status_code, resp.text[:200])
return resp.ok
except Exception as e:
log.error("alias-kv-sync %s failed for %s: %s", action, alias_address, e)
return False
def _alias_to_dict(row):
return {
"address": row["address"],
"mailbox_address": row["mailbox_address"],
"domain": row["domain"],
"label": row["label"],
"description": row["description"],
"is_active": bool(row["is_active"]),
"expires_at": row["expires_at"],
"created_at": row["created_at"],
"use_count": row["use_count"],
"last_use_at": row["last_use_at"],
}
def _caller_mailboxes():
user = request.user
if user.get('role') == 'admin':
return None
return set(user.get('mailboxes', []))
def _check_alias_ownership(alias_row):
user = request.user
if user.get('role') == 'admin':
return True
return alias_row['mailbox_address'] in user.get('mailboxes', [])
@api_bp.route('/aliases', methods=['GET'])
@jwt_required
def list_aliases():
db = get_db()
caller_mailboxes = _caller_mailboxes()
domain_filter = request.args.get('domain', '').strip()
active_filter = request.args.get('active', '')
label_filter = request.args.get('label', '').strip()
mailbox_filter = request.args.get('mailbox', '').strip()
conditions = []
params = []
if caller_mailboxes is not None:
placeholders = ','.join('?' * len(caller_mailboxes))
conditions.append(f"mailbox_address IN ({placeholders})")
params.extend(list(caller_mailboxes))
elif mailbox_filter:
conditions.append("mailbox_address = ?")
params.append(mailbox_filter)
if domain_filter:
conditions.append("domain = ?")
params.append(domain_filter)
if active_filter != '':
conditions.append("is_active = ?")
params.append(1 if active_filter in ('1', 'true') else 0)
if label_filter:
conditions.append("label = ?")
params.append(label_filter)
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
rows = db.execute(f"SELECT * FROM aliases {where} ORDER BY created_at DESC", params).fetchall()
aliases = [_alias_to_dict(r) for r in rows]
now_iso = datetime.now(timezone.utc).isoformat()
total = len(aliases)
active = sum(1 for a in aliases if a['is_active'] and (not a['expires_at'] or a['expires_at'] > now_iso))
expired = sum(1 for a in aliases if a['expires_at'] and a['expires_at'] <= now_iso)
disabled = sum(1 for a in aliases if not a['is_active'])
return jsonify({"aliases": aliases, "total": total, "active": active, "expired": expired, "disabled": disabled})
@api_bp.route('/aliases/generate', methods=['POST'])
@jwt_required
def generate_alias_suggestion():
data = request.json or {}
mailbox_address = (data.get('mailbox_address') or '').strip()
domain = (data.get('domain') or '').strip()
style = data.get('style', 'word-word-num')
if not mailbox_address or not domain:
return jsonify({"error": "mailbox_address and domain are required"}), 400
if style not in ('word-word-num', 'word-num', 'uuid-short'):
return jsonify({"error": "invalid style"}), 400
caller_mailboxes = _caller_mailboxes()
if caller_mailboxes is not None and mailbox_address not in caller_mailboxes:
return jsonify({"error": "forbidden"}), 403
db = get_db()
if not db.execute("SELECT 1 FROM mailboxes WHERE address=? AND domain=?", (mailbox_address, domain)).fetchone():
return jsonify({"error": "mailbox not found for domain"}), 404
suggestion = generate_alias(domain, style=style, db=db)
return jsonify({"suggestion": suggestion, "available": True})
@api_bp.route('/aliases', methods=['POST'])
@jwt_required
def create_alias():
data = request.json or {}
address = (data.get('address') or '').strip().lower()
mailbox_address = (data.get('mailbox_address') or '').strip()
label = (data.get('label') or '').strip() or None
description = (data.get('description') or '').strip()[:200] or None
expires_at = data.get('expires_at') or None
if not address or not mailbox_address:
return jsonify({"error": "address and mailbox_address are required"}), 400
valid, err = validate_alias_address(address)
if not valid:
return jsonify({"error": err}), 400
alias_domain = address.split('@')[1]
caller_mailboxes = _caller_mailboxes()
if caller_mailboxes is not None and mailbox_address not in caller_mailboxes:
return jsonify({"error": "forbidden"}), 403
db = get_db()
if not db.execute("SELECT 1 FROM mailboxes WHERE address=? AND domain=?", (mailbox_address, alias_domain)).fetchone():
return jsonify({"error": "mailbox not found or domain mismatch"}), 403
if db.execute("SELECT 1 FROM aliases WHERE address=?", (address,)).fetchone():
return jsonify({"error": "alias already exists"}), 409
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (address,)).fetchone():
return jsonify({"error": "address already used as mailbox"}), 409
now_iso = datetime.now(timezone.utc).isoformat()
if expires_at:
if not isinstance(expires_at, str) or expires_at <= now_iso:
return jsonify({"error": "expires_at must be a future ISO-8601 datetime"}), 400
cutoff = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
rate_count = db.execute(
"SELECT COUNT(*) FROM aliases WHERE mailbox_address=? AND created_at > ?",
(mailbox_address, cutoff)
).fetchone()[0]
if rate_count >= _ALIAS_RATE_LIMIT_PER_HOUR:
return jsonify({"error": "rate_limit", "message": "max 20 aliases per hour"}), 429
total_count = db.execute(
"SELECT COUNT(*) FROM aliases WHERE mailbox_address=?", (mailbox_address,)
).fetchone()[0]
if total_count >= _ALIAS_MAX_PER_MAILBOX:
return jsonify({"error": "alias_limit_reached", "message": f"max {_ALIAS_MAX_PER_MAILBOX} aliases per mailbox"}), 429
db.execute(
"INSERT INTO aliases (address, mailbox_address, domain, label, description, is_active, expires_at, created_at, use_count) VALUES (?, ?, ?, ?, ?, 1, ?, ?, 0)",
(address, mailbox_address, alias_domain, label, description, expires_at, now_iso)
)
db.commit()
threading.Thread(target=_sync_alias_kv, args=(address, mailbox_address, "put"), daemon=True).start()
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
return jsonify(_alias_to_dict(row)), 201
@api_bp.route('/aliases/<path:address>', methods=['GET'])
@jwt_required
def get_alias(address):
db = get_db()
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
if not _check_alias_ownership(row):
return jsonify({"error": "forbidden"}), 403
return jsonify(_alias_to_dict(row))
@api_bp.route('/aliases/<path:address>', methods=['PATCH'])
@jwt_required
def update_alias(address):
db = get_db()
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
if not _check_alias_ownership(row):
return jsonify({"error": "forbidden"}), 403
data = request.json or {}
updates = {}
kv_action = None
if 'is_active' in data:
new_active = 1 if data['is_active'] else 0
updates['is_active'] = new_active
kv_action = "put" if new_active else "delete"
if 'label' in data:
updates['label'] = (data['label'] or '').strip() or None
if 'description' in data:
updates['description'] = (data['description'] or '').strip()[:200] or None
if 'expires_at' in data:
updates['expires_at'] = data['expires_at'] or None
if not updates:
return jsonify({"error": "no valid fields to update"}), 400
set_clause = ', '.join(f"{k}=?" for k in updates)
db.execute(f"UPDATE aliases SET {set_clause} WHERE address=?", list(updates.values()) + [address])
db.commit()
if kv_action:
mailbox = row['mailbox_address']
threading.Thread(target=_sync_alias_kv, args=(address, mailbox, kv_action), daemon=True).start()
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
return jsonify(_alias_to_dict(row))
@api_bp.route('/aliases/<path:address>', methods=['DELETE'])
@jwt_required
def delete_alias(address):
db = get_db()
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
if not _check_alias_ownership(row):
return jsonify({"error": "forbidden"}), 403
mailbox = row['mailbox_address']
db.execute("DELETE FROM aliases WHERE address=?", (address,))
db.commit()
threading.Thread(target=_sync_alias_kv, args=(address, mailbox, "delete"), daemon=True).start()
return jsonify({"status": "deleted"})

View file

@ -147,6 +147,32 @@ def wipe_all():
config.IN_MAINTENANCE = False
return jsonify({"error": str(e)}), 500
@system_bp.route('/aliases', methods=['GET'])
@admin_required
def list_aliases_system():
domain = request.args.get('domain', '').strip()
db_path = config.DB_PATH
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
if domain:
rows = conn.execute(
"SELECT * FROM aliases WHERE domain=? ORDER BY use_count DESC, created_at DESC",
(domain,)
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM aliases ORDER BY use_count DESC, created_at DESC"
).fetchall()
conn.close()
aliases = [dict(r) for r in rows]
for a in aliases:
a['is_active'] = bool(a['is_active'])
return jsonify({"domain": domain or None, "aliases": aliases, "total": len(aliases)})
except Exception as e:
return jsonify({"error": str(e)}), 500
def _schedule_restart():
def restart():
import time

View file

@ -211,26 +211,55 @@ def inbound():
db = get_db()
to_address = ''
alias_address = None
for addr in parsed['to_addresses']:
cur = db.execute(
"SELECT address FROM mailboxes WHERE address=?", (addr,)
)
if cur.fetchone():
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (addr,)).fetchone():
to_address = addr
break
if not to_address:
via_alias = data.get('via_alias', False)
raw_resolved = data.get('resolved_mailbox', '')
if via_alias and raw_resolved:
envelope_to = data.get('to', '')
now_utc = datetime.now(timezone.utc).isoformat()
alias_row = db.execute(
"SELECT * FROM aliases WHERE address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
(envelope_to, now_utc)
).fetchone()
if alias_row and alias_row['mailbox_address'] == raw_resolved:
to_address = alias_row['mailbox_address']
alias_address = envelope_to
db.execute(
"UPDATE aliases SET use_count=use_count+1, last_use_at=? WHERE address=?",
(now_utc, alias_address)
)
else:
now_utc = datetime.now(timezone.utc).isoformat()
for addr in parsed['to_addresses']:
alias_row = db.execute(
"SELECT * FROM aliases WHERE address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
(addr, now_utc)
).fetchone()
if alias_row:
to_address = alias_row['mailbox_address']
alias_address = addr
db.execute(
"UPDATE aliases SET use_count=use_count+1, last_use_at=? WHERE address=?",
(now_utc, alias_address)
)
break
if not to_address and domain_cfg and domain_cfg['catch_all_mailbox']:
catch_all = domain_cfg['catch_all_mailbox']
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (catch_all,)).fetchone():
to_address = catch_all
if not to_address:
log.info("Inbound ignored: no matching mailbox for %s",
parsed['to_addresses'])
return jsonify({
"status": "ignored",
"reason": "unknown recipient",
}), 200
log.info("Inbound ignored: no matching mailbox for %s", parsed['to_addresses'])
return jsonify({"status": "ignored", "reason": "unknown recipient"}), 200
cur = db.execute(
"SELECT id FROM folders WHERE mailbox_address=? AND name='Inbox'",
@ -253,8 +282,8 @@ def inbound():
to_addresses, cc_addresses, bcc_addresses, subject, text_body,
html_body, received_at, is_read, is_starred, is_draft,
in_reply_to, reference_ids, size_bytes, has_attachments,
headers_json, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?)
headers_json, created_at, received_via_alias
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, ?)
""", (
parsed['message_id'], to_address, folder_id,
parsed['from_address'], parsed['from_name'],
@ -265,7 +294,7 @@ def inbound():
parsed['received_at'], parsed['in_reply_to'],
parsed['references'], actual_size,
1 if parsed['attachments'] else 0,
json.dumps(parsed['headers_json']), now,
json.dumps(parsed['headers_json']), now, alias_address,
))
msg_id = cur.lastrowid
@ -399,6 +428,7 @@ def inbound():
'subject': parsed['subject'],
'from_name': parsed['from_name'] or parsed['from_address'],
'mailbox': to_address,
'via_alias': alias_address,
})
_check_and_send_auto_reply(db, to_address, parsed, domain_cfg)

View file

@ -0,0 +1,92 @@
import logging
import os
from datetime import datetime, timezone
import requests as http_requests
log = logging.getLogger(__name__)
def _sync_alias_kv_delete(alias_address):
master_url = os.environ.get('DOCKFLARE_MASTER_URL', '').rstrip('/')
if not master_url:
return
try:
http_requests.post(
f"{master_url}/email/internal/alias-kv-sync",
json={
"domain": alias_address.split('@')[1],
"alias_address": alias_address,
"action": "delete",
},
headers={"X-Bootstrap-Token": os.environ.get("INTERNAL_BOOTSTRAP_SECRET", "")},
timeout=5,
)
except Exception:
log.error("alias-kv-sync delete failed during expiry for %s", alias_address)
def _insert_expiry_message(db, mailbox_address, alias_address, now):
inbox = db.execute(
"SELECT id FROM folders WHERE mailbox_address=? AND name='Inbox'",
(mailbox_address,)
).fetchone()
if not inbox:
return
alias_row = db.execute(
"SELECT use_count FROM aliases WHERE address=?", (alias_address,)
).fetchone()
if not alias_row or alias_row['use_count'] == 0:
return
db.execute("""
INSERT OR IGNORE INTO messages (
message_id, mailbox_address, folder_id,
from_address, from_name, to_addresses,
cc_addresses, bcc_addresses, subject,
text_body, html_body, received_at,
is_read, is_starred, is_draft,
in_reply_to, reference_ids, size_bytes,
has_attachments, headers_json, created_at, is_system
) VALUES (?, ?, ?, 'noreply@dockflare', 'DockFlare System', ?,
'[]', '[]',
?, ?, '', ?, 0, 0, 0,
NULL, NULL, 0, 0, '{}', ?, 1)
""", (
f"alias-expired-{alias_address}-{now}",
mailbox_address,
inbox['id'],
f'["{mailbox_address}"]',
f"Alias expired: {alias_address}",
f"The alias {alias_address} has reached its expiration date and is no longer active.\n\n"
f"Emails sent to this address will no longer be delivered to your mailbox.",
now,
now,
))
def expire_aliases():
from app.core.database import get_standalone_db
db = get_standalone_db()
try:
now = datetime.now(timezone.utc).isoformat()
expired = db.execute(
"SELECT address, mailbox_address FROM aliases "
"WHERE is_active=1 AND expires_at IS NOT NULL AND expires_at < ?",
(now,)
).fetchall()
if not expired:
return
for alias in expired:
db.execute("UPDATE aliases SET is_active=0 WHERE address=?", (alias['address'],))
_sync_alias_kv_delete(alias['address'])
_insert_expiry_message(db, alias['mailbox_address'], alias['address'], now)
log.info("Alias expired: %s -> %s", alias['address'], alias['mailbox_address'])
db.commit()
except Exception:
log.exception("alias expiry: unhandled error")
finally:
db.close()

View file

@ -0,0 +1,80 @@
import re
import random
from uuid import uuid4
WORDS = [
"able", "acorn", "acre", "aged", "agile", "airy", "alba", "alert", "aloe", "alto",
"amber", "ample", "anchor", "arid", "ariel", "arrow", "aspen", "atlas", "azure", "baron",
"basil", "beam", "bear", "beech", "birch", "blade", "bloom", "blue", "bold", "boon",
"brake", "brave", "briar", "bridge", "bright", "brisk", "brook", "brush", "bulk", "buoy",
"calm", "cape", "cedar", "chalk", "chase", "chime", "chip", "chord", "civic", "clamp",
"clan", "claro", "clean", "clear", "cliff", "cloud", "clove", "coast", "comet", "coral",
"crest", "crisp", "crop", "cross", "crust", "curl", "curve", "damp", "dawn", "deep",
"delta", "dense", "depot", "dew", "dome", "door", "dove", "draft", "dune", "dust",
"eagle", "echo", "edge", "elder", "elm", "ember", "epoch", "equal", "ever", "fable",
"fair", "fawn", "fern", "field", "film", "fine", "fire", "firm", "fixed", "flag",
"flame", "flare", "flat", "fleet", "flint", "float", "flood", "floor", "flow", "foam",
"fond", "ford", "forge", "form", "forte", "frame", "frank", "free", "fresh", "frost",
"full", "gale", "game", "gate", "gaze", "gear", "gild", "glade", "glare", "glow",
"gold", "grade", "grain", "grand", "grant", "grape", "grass", "gravel", "gray", "green",
"grove", "guide", "gulf", "hale", "halt", "haven", "hazel", "helm", "hill", "hive",
"hold", "holly", "honor", "hope", "horn", "hull", "hunt", "inlet", "iron", "isle",
"jade", "jasper", "jest", "jewel", "joint", "jump", "keen", "kelp", "kind", "king",
"kite", "lake", "lance", "lark", "laser", "leaf", "lean", "ledge", "level", "light",
"lime", "link", "lion", "loch", "lodge", "loft", "loom", "loop", "lore", "lotus",
"lunar", "mace", "maple", "marsh", "mast", "meadow", "mesa", "mild", "mint", "mist",
"moat", "mode", "moon", "moor", "moss", "mount", "mural", "naval", "noble", "noon",
"north", "nova", "oaken", "ocean", "olive", "onyx", "open", "orbit", "otter", "ozone",
"pact", "pale", "palm", "panel", "patch", "path", "peak", "pearl", "pine", "plain",
"plank", "plant", "plate", "plaza", "plum", "polar", "pond", "pool", "port", "prime",
"prism", "proof", "pure", "quail", "quartz", "quest", "quiet", "rail", "rain", "ramp",
"range", "rapid", "raven", "realm", "reed", "reef", "reign", "ridge", "rift", "rigid",
"ring", "river", "road", "robin", "rock", "rouge", "round", "route", "ruby", "rune",
"rush", "sage", "sail", "salt", "sand", "satin", "scale", "scope", "scout", "seal",
"shelf", "shim", "shore", "sigil", "silk", "silver", "skiff", "slate", "slick", "slope",
"solar", "solid", "sonic", "span", "spark", "speed", "spire", "spray", "sprint", "spur",
"stark", "star", "steam", "steel", "stern", "still", "stone", "storm", "strand", "stream",
"stripe", "strong", "strut", "sunlit", "swift", "talon", "teal", "terra", "test", "tide",
"timber", "titan", "tonal", "torch", "tower", "trace", "trail", "tram", "trend", "trove",
"trunk", "tulip", "tuned", "ultra", "unity", "valid", "valor", "vault", "veil", "venom",
"virid", "vivid", "vocal", "void", "ward", "wave", "weld", "wheat", "white", "wild",
"willow", "wind", "wing", "winter", "wisp", "wren", "yard", "zeal", "zenith", "zinc",
]
_ADDRESS_RE = re.compile(r'^[a-zA-Z0-9._+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
def validate_alias_address(address):
if not address or not isinstance(address, str):
return False, "address required"
if not _ADDRESS_RE.match(address):
return False, "invalid address format"
local, _, domain = address.partition('@')
if not local or not domain:
return False, "invalid address format"
if '..' in address:
return False, "invalid address format"
return True, None
def generate_alias(domain, style="word-word-num", db=None, max_attempts=20):
for _ in range(max_attempts):
if style == "word-word-num":
candidate = f"{random.choice(WORDS)}.{random.choice(WORDS)}.{random.randint(100, 999)}@{domain}"
elif style == "word-num":
candidate = f"{random.choice(WORDS)}.{random.randint(1000, 9999)}@{domain}"
else:
candidate = f"{uuid4().hex[:10]}@{domain}"
if db is None or not _address_exists(db, candidate):
return candidate
return f"{uuid4().hex[:10]}@{domain}"
def _address_exists(db, address):
if db.execute("SELECT 1 FROM aliases WHERE address=?", (address,)).fetchone():
return True
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (address,)).fetchone():
return True
return False

View file

@ -45,6 +45,7 @@ _SCHEMA = """
has_attachments INTEGER,
headers_json TEXT,
created_at TEXT,
received_via_alias TEXT,
FOREIGN KEY(mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE,
FOREIGN KEY(folder_id) REFERENCES folders(id) ON DELETE CASCADE
);
@ -73,7 +74,8 @@ _SCHEMA = """
sent_at TEXT,
status TEXT,
error_message TEXT,
worker_response TEXT
worker_response TEXT,
via_alias TEXT
);
CREATE TABLE IF NOT EXISTS bounce_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -94,11 +96,28 @@ _SCHEMA = """
outbound_auth_secret TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS aliases (
address TEXT PRIMARY KEY,
mailbox_address TEXT NOT NULL,
domain TEXT NOT NULL,
label TEXT,
description TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
expires_at TEXT,
created_at TEXT NOT NULL,
use_count INTEGER NOT NULL DEFAULT 0,
last_use_at TEXT,
FOREIGN KEY (mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_aliases_mailbox ON aliases(mailbox_address);
CREATE INDEX IF NOT EXISTS idx_aliases_domain ON aliases(domain);
CREATE INDEX IF NOT EXISTS idx_aliases_active ON aliases(is_active, expires_at);
CREATE INDEX IF NOT EXISTS idx_domain_configs_name ON domain_configs(domain_name);
CREATE INDEX IF NOT EXISTS idx_messages_mailbox ON messages(mailbox_address);
CREATE INDEX IF NOT EXISTS idx_messages_folder ON messages(folder_id);
CREATE INDEX IF NOT EXISTS idx_messages_received ON messages(received_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(is_read);
CREATE INDEX IF NOT EXISTS idx_messages_alias ON messages(received_via_alias) WHERE received_via_alias IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id);
CREATE INDEX IF NOT EXISTS idx_send_log_from ON send_log(from_address);
CREATE TABLE IF NOT EXISTS push_subscriptions (
@ -119,7 +138,9 @@ _SCHEMA = """
DROP TRIGGER IF EXISTS messages_ai;
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts(rowid, subject, from_address, from_name, to_addresses, text_body)
VALUES (new.id, new.subject, new.from_address, new.from_name, new.to_addresses, new.text_body);
VALUES (new.id, new.subject, new.from_address, new.from_name,
new.to_addresses || ' ' || COALESCE(new.received_via_alias, ''),
new.text_body);
END;
DROP TRIGGER IF EXISTS messages_ad;
CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
@ -129,7 +150,9 @@ _SCHEMA = """
CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
DELETE FROM messages_fts WHERE rowid = old.id;
INSERT INTO messages_fts(rowid, subject, from_address, from_name, to_addresses, text_body)
VALUES (new.id, new.subject, new.from_address, new.from_name, new.to_addresses, new.text_body);
VALUES (new.id, new.subject, new.from_address, new.from_name,
new.to_addresses || ' ' || COALESCE(new.received_via_alias, ''),
new.text_body);
END;
"""
@ -210,12 +233,36 @@ def _migrate(conn):
"ALTER TABLE mailboxes ADD COLUMN last_quota_warning_at TEXT DEFAULT NULL",
"ALTER TABLE domain_configs ADD COLUMN grace_buffer_bytes INTEGER DEFAULT NULL",
"ALTER TABLE messages ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE messages ADD COLUMN received_via_alias TEXT DEFAULT NULL",
"ALTER TABLE send_log ADD COLUMN via_alias TEXT DEFAULT NULL",
"CREATE INDEX IF NOT EXISTS idx_messages_alias ON messages(received_via_alias) WHERE received_via_alias IS NOT NULL",
]:
try:
conn.execute(sql)
except Exception:
pass
try:
conn.executescript("""
CREATE TABLE IF NOT EXISTS aliases (
address TEXT PRIMARY KEY,
mailbox_address TEXT NOT NULL,
domain TEXT NOT NULL,
label TEXT,
description TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
expires_at TEXT,
created_at TEXT NOT NULL,
use_count INTEGER NOT NULL DEFAULT 0,
last_use_at TEXT,
FOREIGN KEY (mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_aliases_mailbox ON aliases(mailbox_address);
CREATE INDEX IF NOT EXISTS idx_aliases_domain ON aliases(domain);
CREATE INDEX IF NOT EXISTS idx_aliases_active ON aliases(is_active, expires_at);
""")
except Exception:
pass
try:
conn.executescript("""
@ -250,8 +297,8 @@ def init_db():
import logging
os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True)
conn = _connect()
conn.executescript(_SCHEMA)
_migrate(conn)
conn.executescript(_SCHEMA)
result = conn.execute("PRAGMA quick_check").fetchone()
if result and result[0] != 'ok':
logging.getLogger('mail-manager').critical("SQLite integrity check failed: %s", result[0])

View file

@ -5,7 +5,8 @@ import time
log = logging.getLogger(__name__)
_INTERVAL = 7 * 24 * 3600
_CLEANUP_INTERVAL = 7 * 24 * 3600
_ALIAS_EXPIRY_INTERVAL = 3600
def _run_cleanup():
@ -47,7 +48,7 @@ def _run_cleanup():
def _scheduler_loop():
while True:
time.sleep(_INTERVAL)
time.sleep(_CLEANUP_INTERVAL)
log.info("Scheduler: running push subscription cleanup")
try:
_run_cleanup()
@ -55,9 +56,23 @@ def _scheduler_loop():
log.exception("Scheduler: unhandled error in cleanup run")
def _alias_expiry_loop():
time.sleep(300)
while True:
try:
from app.core.alias_expiry import expire_aliases
expire_aliases()
except Exception:
log.exception("Scheduler: unhandled error in alias expiry run")
time.sleep(_ALIAS_EXPIRY_INTERVAL)
def start_scheduler():
if os.environ.get('DISABLE_SCHEDULER'):
return
t = threading.Thread(target=_scheduler_loop, daemon=True, name="push-cleanup-scheduler")
t.start()
log.info("Scheduler: push subscription cleanup scheduled every 7 days")
a = threading.Thread(target=_alias_expiry_loop, daemon=True, name="alias-expiry-scheduler")
a.start()
log.info("Scheduler: alias expiry scheduled every hour")

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<title>DockFlare Mail</title>
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png">

View file

@ -8,6 +8,7 @@
"name": "dockflare-webmail",
"version": "1.0.0",
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@tiptap/extension-color": "^2.27.2",
"@tiptap/extension-font-family": "^2.27.2",
"@tiptap/extension-highlight": "^2.27.2",
@ -25,6 +26,7 @@
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"dompurify": "^3.4.0",
"emoji-mart": "^5.6.0",
"lucide-vue-next": "^0.400.0",
"pinia": "^2.2.0",
"radix-vue": "^1.9.0",
@ -1595,6 +1597,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@emoji-mart/data": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -4285,6 +4293,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",

View file

@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@tiptap/extension-color": "^2.27.2",
"@tiptap/extension-font-family": "^2.27.2",
"@tiptap/extension-highlight": "^2.27.2",
@ -25,6 +26,7 @@
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"dompurify": "^3.4.0",
"emoji-mart": "^5.6.0",
"lucide-vue-next": "^0.400.0",
"pinia": "^2.2.0",
"radix-vue": "^1.9.0",

View file

@ -9,5 +9,17 @@ export const authApi = {
});
return response.json();
},
changePassword: async (currentPassword, newPassword) => {
const token = localStorage.getItem('jwt_token');
const response = await fetch('/email/auth/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
});
return response.json();
},
};
//# sourceMappingURL=auth.js.map

View file

@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;IAED,cAAc,EAAE,KAAK,EAAE,eAAuB,EAAE,WAAmB,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,6BAA6B,EAAE;YAC1D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,eAAe,EAAE,UAAU,KAAK,EAAE;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;SACvF,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}

View file

@ -12,7 +12,6 @@ apiClient.interceptors.request.use(config => {
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Let the browser set Content-Type (with boundary) for FormData
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
@ -21,6 +20,10 @@ apiClient.interceptors.request.use(config => {
apiClient.interceptors.response.use(response => response, error => {
if (error.response?.status === 401) {
localStorage.removeItem('jwt_token');
import('../stores/auth').then(({ useAuthStore }) => {
const authStore = useAuthStore();
authStore.token = '';
});
router.push('/login');
}
return Promise.reject(error);

View file

@ -1 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,gEAAgE;IAChE,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE;YACjD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;YAChC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAA;QACtB,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}

View file

@ -15,9 +15,20 @@ export const mailApi = {
sendMessage: (address, data) => apiClient.post(`/mailboxes/${address}/send`, data),
searchMessages: (address, params) => apiClient.get(`/mailboxes/${address}/search`, { params }),
getMailboxStatus: () => apiClient.get('/mailboxes/status'),
getMailboxPreferences: (address) => apiClient.get(`/mailboxes/${address}/preferences`),
updateMailboxPreferences: (address, data) => apiClient.patch(`/mailboxes/${address}/preferences`, data),
getAttachmentUrl: (id) => `/api/v1/attachments/${id}/download`,
downloadAttachment: (id) => apiClient.get(`/attachments/${id}/download`, { responseType: 'blob' }).then(r => r.data),
createDraft: (address, data) => apiClient.post(`/mailboxes/${address}/drafts`, data),
updateDraft: (address, id, data) => apiClient.put(`/mailboxes/${address}/drafts/${id}`, data),
getAutoResponder: (address) => apiClient.get(`/mailboxes/${address}/auto-responder`),
setAutoResponder: (address, data) => apiClient.post(`/mailboxes/${address}/auto-responder`, data),
deleteAutoResponder: (address) => apiClient.delete(`/mailboxes/${address}/auto-responder`),
getAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
getAllAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox } }),
createAlias: (data) => apiClient.post('/aliases', data),
updateAlias: (address, data) => apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
deleteAlias: (address) => apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
generateAlias: (mailbox, domain, style) => apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
};
//# sourceMappingURL=mail.js.map

View file

@ -1 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}

View file

@ -37,4 +37,16 @@ export const mailApi = {
apiClient.post(`/mailboxes/${address}/auto-responder`, data),
deleteAutoResponder: (address: string) =>
apiClient.delete(`/mailboxes/${address}/auto-responder`),
getAliases: (mailbox: string) =>
apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
getAllAliases: (mailbox: string) =>
apiClient.get('/aliases', { params: { mailbox } }),
createAlias: (data: Record<string, any>) =>
apiClient.post('/aliases', data),
updateAlias: (address: string, data: Record<string, any>) =>
apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
deleteAlias: (address: string) =>
apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
generateAlias: (mailbox: string, domain: string, style?: string) =>
apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
}

View file

@ -2,6 +2,15 @@
@tailwind components;
@tailwind utilities;
@layer utilities {
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.h-\[100dvh\] {
height: 100dvh;
}
}
@layer base {
:root {
--background: 0 0% 100%;

View file

@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, watch, nextTick, onUnmounted } from 'vue'
import { ref, watch, nextTick, onUnmounted, onMounted, type Ref, computed } from 'vue'
import { useBreakpoint } from '../../composables/useBreakpoint'
import {
Paperclip, X, Bold as BoldIcon, Italic as ItalicIcon, Link2, List as ListIcon, ListOrdered, Minus,
Underline as UnderlineIcon, AlignLeft as AlignLeftIcon, AlignCenter as AlignCenterIcon, AlignRight as AlignRightIcon, AlignJustify as AlignJustifyIcon,
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2, Smile
} from 'lucide-vue-next'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
@ -22,11 +23,25 @@ import { useMailStore } from '../../stores/mail'
import Button from '../ui/Button.vue'
import Input from '../ui/Input.vue'
defineProps({ panelMode: { type: Boolean, default: false } })
const props = defineProps({ panelMode: { type: Boolean, default: false } })
const store = useMailStore()
const { isMobile } = useBreakpoint()
const effectivePanelMode = computed(() => props.panelMode || isMobile.value)
const to = ref('')
const _EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const toTags = ref<string[]>([])
const toInput = ref('')
const ccTags = ref<string[]>([])
const ccInput = ref('')
const bccTags = ref<string[]>([])
const bccInput = ref('')
const showCc = ref(false)
const showBcc = ref(false)
const fromAddress = ref('')
const aliases = ref<string[]>([])
const subject = ref('')
const attachments = ref<File[]>([])
const sending = ref(false)
@ -65,8 +80,24 @@ const editor = useEditor({
},
})
const loadAliases = async () => {
if (!store.currentMailbox) return
try {
const res = await mailApi.getAliases(store.currentMailbox)
aliases.value = (res.data.aliases || []).map((a: any) => a.address)
} catch { aliases.value = [] }
}
const reset = () => {
to.value = ''
toTags.value = []
toInput.value = ''
ccTags.value = []
ccInput.value = ''
bccTags.value = []
bccInput.value = ''
showCc.value = false
showBcc.value = false
fromAddress.value = store.currentMailbox || ''
subject.value = ''
attachments.value = []
error.value = ''
@ -78,17 +109,67 @@ const reset = () => {
store.composeDefaults = null
}
const addTag = (tags: Ref<string[]>, input: Ref<string>) => {
const val = input.value.trim().replace(/[,;]+$/, '')
if (val && _EMAIL_RE.test(val) && !tags.value.includes(val)) {
tags.value.push(val)
}
input.value = ''
}
const makeTagHandlers = (tags: Ref<string[]>, input: Ref<string>) => ({
onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
e.preventDefault()
addTag(tags, input)
} else if (e.key === 'Backspace' && !input.value && tags.value.length) {
tags.value.pop()
}
},
onBlur() { addTag(tags, input) },
onPaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData?.getData('text') || ''
for (const addr of text.split(/[,;\s]+/)) {
const trimmed = addr.trim()
if (trimmed && _EMAIL_RE.test(trimmed) && !tags.value.includes(trimmed)) {
tags.value.push(trimmed)
}
}
},
})
const toHandlers = makeTagHandlers(toTags, toInput)
const ccHandlers = makeTagHandlers(ccTags, ccInput)
const bccHandlers = makeTagHandlers(bccTags, bccInput)
onMounted(loadAliases)
watch(() => store.isComposeOpen, async (open) => {
if (open && store.composeDefaults) {
to.value = store.composeDefaults.to || ''
if (open) {
await loadAliases()
if (store.composeDefaults) {
const rawTo = store.composeDefaults.to || ''
if (rawTo) {
for (const addr of rawTo.split(',').map((s: string) => s.trim()).filter(Boolean)) {
if (_EMAIL_RE.test(addr) && !toTags.value.includes(addr)) toTags.value.push(addr)
}
}
subject.value = store.composeDefaults.subject || ''
quotedHtml.value = store.composeDefaults.quotedHtml || ''
if (store.composeDefaults.draftId) {
draftId.value = store.composeDefaults.draftId
}
const requestedFrom = store.composeDefaults.from
fromAddress.value = (requestedFrom && aliases.value.includes(requestedFrom))
? requestedFrom
: (store.currentMailbox || '')
} else {
fromAddress.value = store.currentMailbox || ''
}
minimized.value = false
await nextTick()
if (store.composeDefaults.body) {
if (store.composeDefaults?.body) {
editor.value?.commands.setContent(store.composeDefaults.body)
} else {
editor.value?.commands.clearContent()
@ -98,24 +179,6 @@ watch(() => store.isComposeOpen, async (open) => {
}
}, { immediate: true })
watch(to, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, to: val }
}
})
watch(subject, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, subject: val }
}
})
watch(quotedHtml, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, quotedHtml: val }
}
})
onUnmounted(() => editor.value?.destroy())
const close = () => {
@ -167,24 +230,44 @@ const formatBytes = (bytes: number) => {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
const setLink = () => {
const prev = editor.value?.getAttributes('link').href || ''
const url = window.prompt('Enter URL', prev)
if (url === null) return
if (url === '') {
const showLinkPopover = ref(false)
const linkInput = ref('')
const openLinkPopover = () => {
linkInput.value = editor.value?.getAttributes('link').href || ''
showLinkPopover.value = true
nextTick(() => {
const el = document.getElementById('compose-link-input')
el?.focus()
})
}
const applyLink = () => {
const url = linkInput.value.trim()
if (!url) {
editor.value?.chain().focus().unsetLink().run()
} else {
editor.value?.chain().focus().setLink({ href: url }).run()
const href = url.startsWith('http') ? url : `https://${url}`
editor.value?.chain().focus().setLink({ href }).run()
}
showLinkPopover.value = false
}
const onLinkKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') { e.preventDefault(); applyLink() }
if (e.key === 'Escape') { showLinkPopover.value = false }
}
const saveDraft = async () => {
if (!store.currentMailbox || !editor.value) return
toHandlers.onBlur()
savingDraft.value = true
error.value = ''
try {
const payload = {
to: to.value ? to.value.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
to: toTags.value,
cc: ccTags.value,
bcc: bccTags.value,
subject: subject.value,
html_body: editor.value.getHTML() + (quotedHtml.value || ''),
text_body: editor.value.getText(),
@ -209,6 +292,12 @@ const saveDraft = async () => {
const send = async () => {
if (!store.currentMailbox || !editor.value) return
toHandlers.onBlur()
if (!toTags.value.length) {
error.value = 'Please add at least one recipient.'
return
}
const totalSize = attachments.value.reduce((sum, f) => sum + f.size, 0)
if (totalSize > MAX_ATTACHMENT_BYTES) {
error.value = `Attachments exceed 10 MB limit (${formatBytes(totalSize)} total).`
@ -221,10 +310,15 @@ const send = async () => {
const html = editor.value.getHTML() + (quotedHtml.value || '')
const text = editor.value.getText()
const formData = new FormData()
formData.append('to', to.value)
for (const addr of toTags.value) formData.append('to', addr)
for (const addr of ccTags.value) formData.append('cc', addr)
for (const addr of bccTags.value) formData.append('bcc', addr)
formData.append('subject', subject.value)
formData.append('html', html)
formData.append('text', text)
if (fromAddress.value && fromAddress.value !== store.currentMailbox) {
formData.append('from_address', fromAddress.value)
}
for (const file of attachments.value) {
formData.append('attachments', file)
}
@ -266,18 +360,55 @@ const setHighlight = (e: Event) => {
const target = e.target as HTMLInputElement
editor.value?.chain().focus().setHighlight({ color: target.value }).run()
}
const showEmojiPicker = ref(false)
const emojiPickerContainer = ref<HTMLElement | null>(null)
const openEmojiPicker = async () => {
showEmojiPicker.value = !showEmojiPicker.value
if (!showEmojiPicker.value) return
await nextTick()
if (!emojiPickerContainer.value) return
emojiPickerContainer.value.innerHTML = ''
const { Picker } = await import('emoji-mart')
const data = (await import('@emoji-mart/data')).default
new Picker({
data,
onEmojiSelect: (emoji: any) => {
editor.value?.chain().focus().insertContent(emoji.native).run()
showEmojiPicker.value = false
},
parent: emojiPickerContainer.value,
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
})
}
const onEmojiClickOutside = (e: MouseEvent) => {
if (!emojiPickerContainer.value) return
const wrapper = emojiPickerContainer.value.closest('.emoji-picker-wrapper')
if (wrapper && !wrapper.contains(e.target as Node)) {
showEmojiPicker.value = false
}
}
watch(showEmojiPicker, (val) => {
if (val) document.addEventListener('mousedown', onEmojiClickOutside)
else document.removeEventListener('mousedown', onEmojiClickOutside)
})
onUnmounted(() => document.removeEventListener('mousedown', onEmojiClickOutside))
</script>
<template>
<div
v-if="store.isComposeOpen && (panelMode || !store.isComposeFullView)"
:class="panelMode
v-if="store.isComposeOpen && (effectivePanelMode || !store.isComposeFullView)"
:class="effectivePanelMode
? 'flex flex-col h-full w-full bg-background'
: 'fixed bottom-0 right-6 z-50 flex flex-col rounded-t-xl shadow-2xl border border-border bg-background'"
:style="!panelMode ? (minimized ? 'width:320px' : 'width:560px') : ''"
:style="!effectivePanelMode ? (minimized ? 'width:320px' : 'width:620px') : ''"
>
<!-- Panel mode title bar -->
<div v-if="panelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
<div v-if="effectivePanelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
<span class="flex-1 text-base font-semibold truncate">{{ subject || 'New Message' }}</span>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" title="Pop out" @click="toggleFullView">
<Minimize2 class="size-4" />
@ -288,7 +419,7 @@ const setHighlight = (e: Event) => {
</div>
<!-- Popup mode title bar -->
<div v-else class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
<div v-else-if="!effectivePanelMode" class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
<span class="flex-1 text-sm font-semibold text-primary-foreground truncate">{{ subject || 'New Message' }}</span>
<button type="button" class="rounded p-0.5 text-primary-foreground/70 hover:text-primary-foreground hover:bg-white/10 transition-colors" title="Full view" @click.stop="toggleFullView">
<Maximize2 class="size-4" />
@ -301,14 +432,94 @@ const setHighlight = (e: Event) => {
</button>
</div>
<!-- Body always visible in panel mode, hidden when minimized in popup mode -->
<!-- Body -->
<div
v-show="panelMode || !minimized"
:class="panelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
v-show="effectivePanelMode || !minimized"
:class="effectivePanelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
>
<!-- Fields -->
<div class="flex flex-col border-b border-border flex-shrink-0">
<input v-model="to" placeholder="To" class="w-full border-b border-border px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
<!-- To row -->
<div class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">To</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in toTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="toTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="toInput"
placeholder="Add recipient…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="toHandlers.onKeydown"
@blur="toHandlers.onBlur"
@paste="toHandlers.onPaste"
/>
</div>
<div class="flex items-center gap-2 px-3 py-2 shrink-0">
<button v-if="!showCc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showCc = true">Cc</button>
<button v-if="!showBcc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showBcc = true">Bcc</button>
</div>
</div>
<!-- Cc row -->
<div v-if="showCc" class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Cc</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in ccTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="ccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="ccInput"
placeholder="Add Cc…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="ccHandlers.onKeydown"
@blur="ccHandlers.onBlur"
@paste="ccHandlers.onPaste"
/>
</div>
</div>
<!-- Bcc row -->
<div v-if="showBcc" class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Bcc</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in bccTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="bccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="bccInput"
placeholder="Add Bcc…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="bccHandlers.onKeydown"
@blur="bccHandlers.onBlur"
@paste="bccHandlers.onPaste"
/>
</div>
</div>
<!-- From row (aliases) -->
<div v-if="aliases.length" class="flex items-center border-b border-border">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0">From</span>
<select v-model="fromAddress" class="flex-1 px-2 py-2 text-sm bg-background text-foreground focus:outline-none">
<option :value="store.currentMailbox">{{ store.currentMailbox }}</option>
<option v-for="alias in aliases" :key="alias" :value="alias">{{ alias }} (alias)</option>
</select>
</div>
<!-- Subject -->
<input v-model="subject" placeholder="Subject" class="w-full px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
</div>
@ -330,7 +541,7 @@ const setHighlight = (e: Event) => {
<div v-if="error" class="px-4 py-1 text-xs text-red-500 flex-shrink-0">{{ error }}</div>
<!-- Formatting Toolbar (Collapsible) -->
<!-- Formatting Toolbar -->
<div v-if="showFormatting" class="flex flex-wrap items-center gap-1 border-t border-border bg-muted/30 px-3 py-1.5 flex-shrink-0">
<select @change="setFont" class="text-xs bg-transparent border-none focus:ring-0 text-foreground cursor-pointer mr-1 max-w-[100px]">
<option value="">Default Font</option>
@ -364,12 +575,18 @@ const setHighlight = (e: Event) => {
<!-- Bottom Action Bar -->
<div class="flex items-center justify-between gap-2 border-t border-border px-4 py-2.5 flex-shrink-0 bg-background">
<div class="flex items-center gap-1">
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || !to">
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || (!toTags.length && !toInput)">
{{ sending ? 'Sending…' : 'Send' }}
</Button>
<button type="button" class="ml-1 rounded p-1.5 transition-colors" :class="savedDraft ? 'text-green-500' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" :disabled="savingDraft" title="Save draft" @click="saveDraft">
<BookmarkCheck class="size-4" />
</button>
<div class="relative emoji-picker-wrapper">
<button type="button" class="rounded p-1.5 transition-colors" :class="showEmojiPicker ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openEmojiPicker" title="Insert emoji">
<Smile class="size-4" />
</button>
<div v-if="showEmojiPicker" class="absolute bottom-10 left-0 z-50 shadow-xl rounded-xl overflow-hidden" ref="emojiPickerContainer" />
</div>
<button type="button" class="ml-1 rounded p-1.5 hover:bg-accent transition-colors" :class="showFormatting ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" @click="showFormatting = !showFormatting" title="Formatting options">
<Type class="size-4" />
</button>
@ -377,9 +594,26 @@ const setHighlight = (e: Event) => {
<Paperclip class="size-4" />
<input type="file" multiple class="hidden" @change="onFileChange" />
</label>
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" @click="setLink" title="Insert link">
<div class="relative">
<button type="button" class="rounded p-1.5 transition-colors" :class="showLinkPopover ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openLinkPopover" title="Insert link">
<Link2 class="size-4" />
</button>
<div v-if="showLinkPopover" class="absolute bottom-10 left-0 z-50 w-72 rounded-lg border border-border bg-background shadow-xl p-3 flex flex-col gap-2">
<label class="text-xs font-medium text-muted-foreground">Insert link</label>
<input
id="compose-link-input"
v-model="linkInput"
type="url"
placeholder="https://example.com"
class="w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
@keydown="onLinkKeydown"
/>
<div class="flex gap-2 justify-end">
<button type="button" class="rounded-md px-3 py-1 text-xs text-muted-foreground hover:bg-accent transition-colors" @click="showLinkPopover = false">Cancel</button>
<button type="button" class="rounded-md px-3 py-1 text-xs bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" @click="applyLink">Apply</button>
</div>
</div>
</div>
</div>
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-destructive transition-colors" :title="draftId ? 'Delete draft' : 'Discard'" @click="discardDraft">
<Trash2 class="size-4" />
@ -390,7 +624,6 @@ const setHighlight = (e: Event) => {
</template>
<style>
/* Tiptap editor inside compose */
.compose-editor {
display: flex;
flex-direction: column;
@ -433,7 +666,6 @@ const setHighlight = (e: Event) => {
cursor: pointer;
}
/* Pop-up animation */
.compose-pop-enter-active { transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease; }
.compose-pop-leave-active { transition: transform 0.14s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.14s ease; }
.compose-pop-enter-from { transform: translateY(24px) scale(0.95); opacity: 0; }

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -48,8 +48,8 @@ const confirmNewFolder = async () => {
store.folders = res.data;
showNewFolder.value = false;
}
catch (e) {
console.error('Failed to create folder', e);
catch {
store.showToast('Failed to create folder');
}
finally {
creatingFolder.value = false;
@ -69,8 +69,8 @@ const deleteFolder = async (f) => {
store.currentFolder = store.folders[0]?.name || '';
}
}
catch (e) {
console.error('Failed to delete folder', e);
catch {
store.showToast('Failed to delete folder');
}
};
// ── Folder rename / colour edit ──────────────────────────────────────
@ -100,8 +100,8 @@ const confirmEdit = async () => {
}
editingFolder.value = null;
}
catch (e) {
console.error('Failed to rename folder', e);
catch {
store.showToast('Failed to rename folder');
}
};
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
@ -182,6 +182,7 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
}, ...__VLS_functionalComponentArgsRest(__VLS_17));
__VLS_19.slots.default;
(f.name);
if (f.total_count > 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "ml-auto text-muted-foreground flex gap-1" },
});
@ -192,7 +193,8 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
(f.unread_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count || 0);
(f.total_count);
}
var __VLS_19;
var __VLS_15;
var __VLS_3;
@ -279,6 +281,7 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
...{ class: "truncate" },
});
(f.name);
if (f.total_count > 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (__VLS_ctx.cn('ml-auto text-xs flex-shrink-0 flex gap-1', __VLS_ctx.store.currentFolder === f.name ? 'text-primary-foreground' : 'text-muted-foreground')) },
});
@ -289,7 +292,8 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
(f.unread_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count || 0);
(f.total_count);
}
if (!f.system_folder) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: (__VLS_ctx.cn('absolute right-1 flex gap-0.5 opacity-0 group-hover/row:opacity-100 transition-opacity rounded px-0.5', __VLS_ctx.store.currentFolder === f.name ? 'bg-primary' : 'bg-accent')) },
@ -372,6 +376,16 @@ if (__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed) {
...{ style: (`background:${c}; border-color:${__VLS_ctx.newFolderColor === c ? '#000' : 'transparent'}`) },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed))
return;
__VLS_ctx.newFolderColor = '';
} },
...{ class: "h-5 w-5 rounded-full border-2 text-xs flex items-center justify-center text-muted-foreground hover:bg-accent" },
...{ style: (`border-color:${!__VLS_ctx.newFolderColor ? '#888' : 'transparent'}`) },
title: "No colour",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 justify-end" },
});
@ -531,6 +545,16 @@ if (!__VLS_ctx.isCollapsed) {
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-transform']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:scale-110']} */ ;
/** @type {__VLS_StyleScopedClasses['h-5']} */ ;
/** @type {__VLS_StyleScopedClasses['w-5']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;

File diff suppressed because one or more lines are too long

View file

@ -3,10 +3,9 @@ import {
SplitterGroup, SplitterPanel, SplitterResizeHandle,
TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal,
} from 'radix-vue'
import { defineAsyncComponent } from 'vue'
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2 } from 'lucide-vue-next'
import { defineAsyncComponent, ref, watch, computed } from 'vue'
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2, ChevronLeft, Menu } from 'lucide-vue-next'
import { cn } from '../../lib/utils'
import Separator from '../ui/Separator.vue'
import MailboxSelector from './MailboxSelector.vue'
import FolderNav from './FolderNav.vue'
import MessageList from './MessageList.vue'
@ -14,11 +13,13 @@ import MessageDisplay from './MessageDisplay.vue'
import ComposeDialog from './ComposeDialog.vue'
import { useMailStore } from '../../stores/mail'
import { useAuth } from '../../composables/useAuth'
import { useBreakpoint } from '../../composables/useBreakpoint'
const SettingsDialog = defineAsyncComponent(() => import('./SettingsDialog.vue'))
const store = useMailStore()
const { logout } = useAuth()
const { isMobile } = useBreakpoint()
const onCollapse = () => { store.isCollapsed = true }
const onExpand = () => { store.isCollapsed = false }
@ -27,11 +28,134 @@ const compose = () => {
store.composeDefaults = null
store.isComposeOpen = true
}
// Mobile navigation stack
type MobilePanel = 'folders' | 'list' | 'detail'
const mobilePanel = ref<MobilePanel>('list')
watch(() => store.currentFolder, () => {
if (isMobile.value) mobilePanel.value = 'list'
})
watch(() => store.currentMessage, (msg) => {
if (isMobile.value && msg) mobilePanel.value = 'detail'
})
const goBack = () => {
if (mobilePanel.value === 'detail') {
store.currentMessage = null
mobilePanel.value = 'list'
} else if (mobilePanel.value === 'list') {
mobilePanel.value = 'folders'
}
}
const mobileTitle = computed(() => {
if (mobilePanel.value === 'folders') return store.currentMailbox || 'Folders'
if (mobilePanel.value === 'list') return store.currentFolder || 'Inbox'
return store.currentMessage?.subject || 'Message'
})
</script>
<template>
<TooltipProvider :delay-duration="0">
<!--
MOBILE LAYOUT (< 768px)
-->
<div v-if="isMobile" class="flex flex-col h-[100dvh] w-screen bg-background overflow-hidden">
<!-- Top bar -->
<div class="h-14 flex items-center gap-2 px-3 border-b border-border flex-shrink-0 bg-background">
<button
v-if="mobilePanel !== 'folders'"
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="goBack"
>
<ChevronLeft class="size-5" />
</button>
<button
v-else
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="store.isSettingsOpen = true"
>
<Settings class="size-4" />
</button>
<span class="flex-1 text-base font-semibold truncate">{{ mobileTitle }}</span>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="store.toggleTheme()"
>
<Sun v-if="store.isDark" class="size-4" />
<Moon v-else class="size-4" />
</button>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="logout"
>
<LogOut class="size-4" />
</button>
</div>
<!-- Panel content -->
<div class="flex-1 min-h-0 overflow-hidden">
<!-- Folders panel -->
<div v-if="mobilePanel === 'folders'" class="h-full flex flex-col overflow-y-auto">
<div class="px-3 py-3 border-b border-border">
<MailboxSelector :is-collapsed="false" />
</div>
<FolderNav :is-collapsed="false" />
</div>
<!-- Message list panel -->
<div v-else-if="mobilePanel === 'list'" class="h-full flex flex-col overflow-hidden">
<MessageList />
</div>
<!-- Message detail panel -->
<div v-else-if="mobilePanel === 'detail'" class="h-full flex flex-col overflow-hidden">
<MessageDisplay :message="store.currentMessage ?? undefined" />
</div>
</div>
<!-- Bottom nav bar -->
<div class="h-16 flex items-center justify-around border-t border-border flex-shrink-0 bg-background pb-safe">
<button
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
:class="mobilePanel === 'folders' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
@click="mobilePanel = 'folders'"
>
<Menu class="size-5" />
<span class="text-[10px] font-medium">Folders</span>
</button>
<button
class="flex items-center justify-center h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors"
@click="compose"
>
<PenSquare class="size-5" />
</button>
<button
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
:class="mobilePanel === 'list' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
@click="mobilePanel = 'list'"
>
<Columns2 class="size-5" />
<span class="text-[10px] font-medium">Mail</span>
</button>
</div>
</div>
<!--
DESKTOP LAYOUT ( 768px) unchanged
-->
<SplitterGroup
v-else
id="mail-layout"
direction="horizontal"
class="h-screen w-screen items-stretch"
@ -50,7 +174,6 @@ const compose = () => {
@collapse="onCollapse"
@expand="onExpand"
>
<!-- Single header row (same height as middle + right panel headers) -->
<div
:class="cn(
'h-[52px] flex items-center gap-1 px-2 flex-shrink-0',
@ -58,11 +181,9 @@ const compose = () => {
)"
>
<template v-if="!store.isCollapsed">
<!-- Mailbox selector fills available space -->
<div class="flex-1 min-w-0">
<MailboxSelector :is-collapsed="false" />
</div>
<!-- Compose -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -74,7 +195,6 @@ const compose = () => {
</button>
</TooltipTrigger>
</TooltipRoot>
<!-- View Mode toggle -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -91,7 +211,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Theme toggle -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -108,7 +227,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Settings -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -124,7 +242,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Logout -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -142,7 +259,6 @@ const compose = () => {
</TooltipRoot>
</template>
<!-- Collapsed: stacked icon buttons -->
<template v-else>
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
@ -185,7 +301,6 @@ const compose = () => {
<TooltipContent side="right" class="z-50 rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md">Logout</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Collapsed mailbox icon below -->
<MailboxSelector :is-collapsed="true" />
</template>
</div>
@ -242,7 +357,15 @@ const compose = () => {
</template>
</SplitterGroup>
<!-- Floating compose (desktop only) + settings dialog -->
<template v-if="!isMobile">
<ComposeDialog />
</template>
<Teleport v-else to="body">
<div v-if="store.isComposeOpen" class="fixed inset-0 z-50 flex flex-col bg-background">
<ComposeDialog :panel-mode="true" />
</div>
</Teleport>
<SettingsDialog />
</TooltipProvider>
</template>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -81,6 +81,15 @@ watch(emailIframe, (el) => {
onUnmounted(() => resizeObserver?.disconnect())
const parseAddrs = (raw: string | null | undefined) => {
let addrs: string[] = []
try { addrs = JSON.parse(raw || '[]') } catch { addrs = [] }
return addrs.map((a: string) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a }).join(', ')
}
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses))
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses))
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses))
const displayTimestamp = computed(() => {
const ts = props.message?.received_at || props.message?.sent_at
return ts ? format(new Date(ts), 'PPpp') : ''
@ -104,6 +113,7 @@ const replyTo = () => {
if (!props.message) return
store.composeDefaults = {
to: props.message.from_address,
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -126,6 +136,7 @@ const replyAll = () => {
].filter((a: string) => a && a !== store.currentMailbox)
store.composeDefaults = {
to: allAddresses.join(', '),
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -495,10 +506,16 @@ const sendInlineReply = async () => {
<Avatar :initials="message.from_name?.[0] || message.from_address?.[0] || '?'" />
<div class="grid gap-1">
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
<div class="line-clamp-1 text-xs">
<span class="font-medium">Reply-To:</span> {{ message.from_address }}
<div v-if="toDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">To:</span> {{ toDisplay }}
</div>
<div v-if="ccDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">Cc:</span> {{ ccDisplay }}
</div>
<div v-if="bccDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">Bcc:</span> {{ bccDisplay }}
</div>
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
</div>
</div>
<div v-if="displayTimestamp" class="ml-auto text-xs text-muted-foreground">

View file

@ -65,6 +65,19 @@ watch(emailIframe, (el) => {
resizeObserver.observe(el.parentElement);
});
onUnmounted(() => resizeObserver?.disconnect());
const parseAddrs = (raw) => {
let addrs = [];
try {
addrs = JSON.parse(raw || '[]');
}
catch {
addrs = [];
}
return addrs.map((a) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a; }).join(', ');
};
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses));
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses));
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses));
const displayTimestamp = computed(() => {
const ts = props.message?.received_at || props.message?.sent_at;
return ts ? format(new Date(ts), 'PPpp') : '';
@ -85,6 +98,7 @@ const replyTo = () => {
return;
store.composeDefaults = {
to: props.message.from_address,
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -96,13 +110,28 @@ const replyTo = () => {
const replyAll = () => {
if (!props.message)
return;
let toList = [];
let ccList = [];
try {
toList = JSON.parse(props.message.to_addresses || '[]');
}
catch {
toList = [];
}
try {
ccList = JSON.parse(props.message.cc_addresses || '[]');
}
catch {
ccList = [];
}
const allAddresses = [
props.message.from_address,
...(JSON.parse(props.message.to_addresses || '[]')),
...(JSON.parse(props.message.cc_addresses || '[]')),
...toList,
...ccList,
].filter((a) => a && a !== store.currentMailbox);
store.composeDefaults = {
to: allAddresses.join(', '),
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -134,9 +163,11 @@ const trash = async () => {
await mailApi.deleteMessage(store.currentMailbox, props.message.id);
store.messages = store.messages.filter((m) => m.id !== props.message.id);
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to trash message', e);
catch {
store.showToast('Failed to move message to trash');
}
};
const markUnread = async () => {
@ -147,10 +178,28 @@ const markUnread = async () => {
const idx = store.messages.findIndex((m) => m.id === props.message.id);
if (idx !== -1)
store.messages[idx] = { ...store.messages[idx], is_read: 0 };
store.currentMessage = null;
store.currentMessage = { ...store.currentMessage, is_read: 0 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to mark unread', e);
catch {
store.showToast('Failed to mark as unread');
}
};
const markRead = async () => {
if (!props.message || !store.currentMailbox)
return;
try {
await mailApi.updateMessage(store.currentMailbox, props.message.id, { is_read: true });
const idx = store.messages.findIndex((m) => m.id === props.message.id);
if (idx !== -1)
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
store.currentMessage = { ...store.currentMessage, is_read: 1 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch {
store.showToast('Failed to mark as read');
}
};
const toggleStar = async () => {
@ -165,8 +214,8 @@ const toggleStar = async () => {
if (store.currentMessage)
store.currentMessage = { ...store.currentMessage, is_starred: newVal };
}
catch (e) {
console.error('Failed to toggle star', e);
catch {
store.showToast('Failed to update star');
}
};
const moveToFolder = async (targetFolder) => {
@ -179,17 +228,26 @@ const moveToFolder = async (targetFolder) => {
});
store.messages = store.messages.filter((m) => m.id !== props.message.id);
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to move message', e);
catch {
store.showToast('Failed to move message');
}
};
const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const printMessage = () => {
if (!props.message)
return;
const from = props.message.from_name ? `${props.message.from_name} <${props.message.from_address}>` : props.message.from_address;
const toRaw = JSON.parse(props.message.to_addresses || '[]');
const to = Array.isArray(toRaw) ? toRaw.join(', ') : toRaw;
let toRaw = [];
try {
toRaw = JSON.parse(props.message.to_addresses || '[]');
}
catch {
toRaw = [];
}
const to = Array.isArray(toRaw) ? toRaw.join(', ') : String(toRaw);
const date = displayTimestamp.value;
const subject = props.message.subject || '(No Subject)';
let content = '';
@ -225,9 +283,9 @@ const printMessage = () => {
</head>
<body>
<div class="header">
<h1 class="subject">${subject}</h1>
<div class="meta"><div class="label">From:</div><div class="val">${from.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></div>
<div class="meta"><div class="label">To:</div><div class="val">${to.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></div>
<h1 class="subject">${escapeHtml(subject)}</h1>
<div class="meta"><div class="label">From:</div><div class="val">${escapeHtml(from)}</div></div>
<div class="meta"><div class="label">To:</div><div class="val">${escapeHtml(to)}</div></div>
<div class="meta"><div class="label">Date:</div><div class="val">${date}</div></div>
</div>
<div class="content">
@ -248,6 +306,8 @@ const printMessage = () => {
const sendInlineReply = async () => {
if (!props.message || !store.currentMailbox || !replyText.value.trim())
return;
if (!props.message.from_address)
return;
sendingReply.value = true;
try {
await mailApi.sendMessage(store.currentMailbox, {
@ -261,8 +321,8 @@ const sendInlineReply = async () => {
});
replyText.value = '';
}
catch (e) {
console.error('Failed to send reply', e);
catch {
store.showToast('Failed to send reply');
}
finally {
sendingReply.value = false;
@ -882,6 +942,7 @@ const __VLS_223 = __VLS_222({
...{ class: "z-50 min-w-[160px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}, ...__VLS_functionalComponentArgsRest(__VLS_222));
__VLS_224.slots.default;
if (props.message?.is_read) {
const __VLS_225 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
@ -910,6 +971,8 @@ const __VLS_235 = __VLS_234({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_234));
var __VLS_228;
}
else {
const __VLS_237 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
@ -925,11 +988,11 @@ let __VLS_241;
let __VLS_242;
let __VLS_243;
const __VLS_244 = {
onClick: (__VLS_ctx.toggleStar)
onClick: (__VLS_ctx.markRead)
};
__VLS_240.slots.default;
const __VLS_245 = {}.Star;
/** @type {[typeof __VLS_components.Star, ]} */ ;
const __VLS_245 = {}.MailOpen;
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
// @ts-ignore
const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
...{ class: "mr-2 size-4" },
@ -937,82 +1000,111 @@ const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
const __VLS_247 = __VLS_246({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_246));
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
var __VLS_240;
const __VLS_249 = {}.DropdownMenuSeparator;
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
}
const __VLS_249 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_250 = __VLS_asFunctionalComponent(__VLS_249, new __VLS_249({
...{ class: "my-1 h-px bg-border" },
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_251 = __VLS_250({
...{ class: "my-1 h-px bg-border" },
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_250));
const __VLS_253 = {}.DropdownMenuSub;
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
// @ts-ignore
const __VLS_254 = __VLS_asFunctionalComponent(__VLS_253, new __VLS_253({}));
const __VLS_255 = __VLS_254({}, ...__VLS_functionalComponentArgsRest(__VLS_254));
__VLS_256.slots.default;
const __VLS_257 = {}.DropdownMenuSubTrigger;
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
let __VLS_253;
let __VLS_254;
let __VLS_255;
const __VLS_256 = {
onClick: (__VLS_ctx.toggleStar)
};
__VLS_252.slots.default;
const __VLS_257 = {}.Star;
/** @type {[typeof __VLS_components.Star, ]} */ ;
// @ts-ignore
const __VLS_258 = __VLS_asFunctionalComponent(__VLS_257, new __VLS_257({
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
...{ class: "mr-2 size-4" },
}));
const __VLS_259 = __VLS_258({
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_258));
__VLS_260.slots.default;
const __VLS_261 = {}.FolderInput;
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
var __VLS_252;
const __VLS_261 = {}.DropdownMenuSeparator;
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
// @ts-ignore
const __VLS_262 = __VLS_asFunctionalComponent(__VLS_261, new __VLS_261({
...{ class: "mr-2 size-4" },
...{ class: "my-1 h-px bg-border" },
}));
const __VLS_263 = __VLS_262({
...{ class: "mr-2 size-4" },
...{ class: "my-1 h-px bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_262));
var __VLS_260;
const __VLS_265 = {}.DropdownMenuPortal;
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
const __VLS_265 = {}.DropdownMenuSub;
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
// @ts-ignore
const __VLS_266 = __VLS_asFunctionalComponent(__VLS_265, new __VLS_265({}));
const __VLS_267 = __VLS_266({}, ...__VLS_functionalComponentArgsRest(__VLS_266));
__VLS_268.slots.default;
const __VLS_269 = {}.DropdownMenuSubContent;
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
const __VLS_269 = {}.DropdownMenuSubTrigger;
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
// @ts-ignore
const __VLS_270 = __VLS_asFunctionalComponent(__VLS_269, new __VLS_269({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_271 = __VLS_270({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_270));
__VLS_272.slots.default;
for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
const __VLS_273 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
const __VLS_273 = {}.FolderInput;
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
// @ts-ignore
const __VLS_274 = __VLS_asFunctionalComponent(__VLS_273, new __VLS_273({
...{ class: "mr-2 size-4" },
}));
const __VLS_275 = __VLS_274({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
var __VLS_272;
const __VLS_277 = {}.DropdownMenuPortal;
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
// @ts-ignore
const __VLS_278 = __VLS_asFunctionalComponent(__VLS_277, new __VLS_277({}));
const __VLS_279 = __VLS_278({}, ...__VLS_functionalComponentArgsRest(__VLS_278));
__VLS_280.slots.default;
const __VLS_281 = {}.DropdownMenuSubContent;
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
// @ts-ignore
const __VLS_282 = __VLS_asFunctionalComponent(__VLS_281, new __VLS_281({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}));
const __VLS_283 = __VLS_282({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}, ...__VLS_functionalComponentArgsRest(__VLS_282));
__VLS_284.slots.default;
for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
const __VLS_285 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_286 = __VLS_asFunctionalComponent(__VLS_285, new __VLS_285({
...{ 'onClick': {} },
key: (f.id),
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_275 = __VLS_274({
const __VLS_287 = __VLS_286({
...{ 'onClick': {} },
key: (f.id),
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
let __VLS_277;
let __VLS_278;
let __VLS_279;
const __VLS_280 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_286));
let __VLS_289;
let __VLS_290;
let __VLS_291;
const __VLS_292 = {
onClick: (...[$event]) => {
__VLS_ctx.moveToFolder(f);
}
};
__VLS_276.slots.default;
__VLS_288.slots.default;
if (f.color) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
...{ class: "mr-2 inline-block h-2 w-2 rounded-full flex-shrink-0" },
@ -1020,11 +1112,11 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
});
}
(f.name);
var __VLS_276;
var __VLS_288;
}
var __VLS_272;
var __VLS_284;
var __VLS_280;
var __VLS_268;
var __VLS_256;
var __VLS_224;
var __VLS_220;
var __VLS_205;
@ -1052,12 +1144,12 @@ if (__VLS_ctx.message) {
});
/** @type {[typeof Avatar, ]} */ ;
// @ts-ignore
const __VLS_281 = __VLS_asFunctionalComponent(Avatar, new Avatar({
const __VLS_293 = __VLS_asFunctionalComponent(Avatar, new Avatar({
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
}));
const __VLS_282 = __VLS_281({
const __VLS_294 = __VLS_293({
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
}, ...__VLS_functionalComponentArgsRest(__VLS_281));
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid gap-1" },
});
@ -1065,17 +1157,37 @@ if (__VLS_ctx.message) {
...{ class: "font-semibold" },
});
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
(__VLS_ctx.message.subject);
if (__VLS_ctx.toDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.message.from_address);
(__VLS_ctx.toDisplay);
}
if (__VLS_ctx.ccDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.ccDisplay);
}
if (__VLS_ctx.bccDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.bccDisplay);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
(__VLS_ctx.message.subject);
if (__VLS_ctx.displayTimestamp) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "ml-auto text-xs text-muted-foreground" },
@ -1084,8 +1196,8 @@ if (__VLS_ctx.message) {
}
/** @type {[typeof Separator, ]} */ ;
// @ts-ignore
const __VLS_284 = __VLS_asFunctionalComponent(Separator, new Separator({}));
const __VLS_285 = __VLS_284({}, ...__VLS_functionalComponentArgsRest(__VLS_284));
const __VLS_296 = __VLS_asFunctionalComponent(Separator, new Separator({}));
const __VLS_297 = __VLS_296({}, ...__VLS_functionalComponentArgsRest(__VLS_296));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex-1 overflow-y-auto" },
});
@ -1109,20 +1221,20 @@ if (__VLS_ctx.message) {
}
/** @type {[typeof AttachmentBar, ]} */ ;
// @ts-ignore
const __VLS_287 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
const __VLS_299 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
attachments: (__VLS_ctx.message.attachments),
}));
const __VLS_288 = __VLS_287({
const __VLS_300 = __VLS_299({
attachments: (__VLS_ctx.message.attachments),
}, ...__VLS_functionalComponentArgsRest(__VLS_287));
}, ...__VLS_functionalComponentArgsRest(__VLS_299));
/** @type {[typeof Separator, ]} */ ;
// @ts-ignore
const __VLS_290 = __VLS_asFunctionalComponent(Separator, new Separator({
const __VLS_302 = __VLS_asFunctionalComponent(Separator, new Separator({
...{ class: "mt-auto print-hide" },
}));
const __VLS_291 = __VLS_290({
const __VLS_303 = __VLS_302({
...{ class: "mt-auto print-hide" },
}, ...__VLS_functionalComponentArgsRest(__VLS_290));
}, ...__VLS_functionalComponentArgsRest(__VLS_302));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 print-hide" },
});
@ -1134,36 +1246,36 @@ if (__VLS_ctx.message) {
});
/** @type {[typeof Textarea, ]} */ ;
// @ts-ignore
const __VLS_293 = __VLS_asFunctionalComponent(Textarea, new Textarea({
const __VLS_305 = __VLS_asFunctionalComponent(Textarea, new Textarea({
modelValue: (__VLS_ctx.replyText),
...{ class: "p-4 min-h-[100px]" },
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
}));
const __VLS_294 = __VLS_293({
const __VLS_306 = __VLS_305({
modelValue: (__VLS_ctx.replyText),
...{ class: "p-4 min-h-[100px]" },
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
}, ...__VLS_functionalComponentArgsRest(__VLS_305));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center" },
});
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_296 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_308 = __VLS_asFunctionalComponent(Button, new Button({
type: "submit",
size: "sm",
...{ class: "ml-auto" },
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
}));
const __VLS_297 = __VLS_296({
const __VLS_309 = __VLS_308({
type: "submit",
size: "sm",
...{ class: "ml-auto" },
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
}, ...__VLS_functionalComponentArgsRest(__VLS_296));
__VLS_298.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_308));
__VLS_310.slots.default;
(__VLS_ctx.sendingReply ? 'Sending...' : 'Send');
var __VLS_298;
var __VLS_310;
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
@ -1298,6 +1410,19 @@ else {
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['my-1']} */ ;
/** @type {__VLS_StyleScopedClasses['h-px']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-border']} */ ;
@ -1363,9 +1488,15 @@ else {
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
@ -1434,6 +1565,9 @@ const __VLS_self = (await import('vue')).defineComponent({
emailIframe: emailIframe,
safeHtml: safeHtml,
resizeIframe: resizeIframe,
toDisplay: toDisplay,
ccDisplay: ccDisplay,
bccDisplay: bccDisplay,
displayTimestamp: displayTimestamp,
otherFolders: otherFolders,
replyTo: replyTo,
@ -1442,6 +1576,7 @@ const __VLS_self = (await import('vue')).defineComponent({
backToList: backToList,
trash: trash,
markUnread: markUnread,
markRead: markRead,
toggleStar: toggleStar,
moveToFolder: moveToFolder,
printMessage: printMessage,

File diff suppressed because one or more lines are too long

View file

@ -1,31 +1,21 @@
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref, computed } from 'vue';
import { Search, ArrowDownUp, Trash2 } from 'lucide-vue-next';
import { ArrowDownUp, Trash2 } from 'lucide-vue-next';
import { TabsRoot, TabsList, TabsTrigger, TabsContent, } from 'radix-vue';
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb, } from 'radix-vue';
import { useMailStore } from '../../stores/mail';
import { mailApi } from '../../api/mail';
import MessageListItem from './MessageListItem.vue';
import Input from '../ui/Input.vue';
import SearchBar from './SearchBar.vue';
import Dialog from '../ui/Dialog.vue';
import Button from '../ui/Button.vue';
const store = useMailStore();
const searchValue = ref('');
const showTrashConfirm = ref(false);
const folderColor = computed(() => store.currentFolderObj?.color || '');
const filteredMessages = computed(() => {
const q = searchValue.value.trim().toLowerCase();
if (!q)
return store.messages;
return store.messages.filter((m) => (m.from_name || '').toLowerCase().includes(q) ||
(m.from_address || '').toLowerCase().includes(q) ||
(m.subject || '').toLowerCase().includes(q) ||
(m.text_body || '').toLowerCase().includes(q));
});
const unreadMessages = computed(() => filteredMessages.value.filter((m) => !m.is_read));
const starredMessages = computed(() => filteredMessages.value.filter((m) => m.is_starred));
const unreadMessages = computed(() => store.messages.filter((m) => !m.is_read));
const starredMessages = computed(() => store.messages.filter((m) => m.is_starred));
const displayMessages = computed(() => {
let msgs = filteredMessages.value;
let msgs = store.messages;
if (store.activeTab === 'unread')
msgs = unreadMessages.value;
else if (store.activeTab === 'starred')
@ -68,18 +58,21 @@ const emptyTrash = () => {
}
};
const performEmptyTrash = async () => {
if (store.currentFolderObj) {
if (!store.currentFolderObj)
return;
try {
await mailApi.emptyFolder(store.currentMailbox, store.currentFolderObj.id);
store.messages = [];
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
catch {
store.showToast('Failed to empty trash');
}
finally {
showTrashConfirm.value = false;
}
}
};
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
@ -195,44 +188,34 @@ var __VLS_15;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative" },
});
const __VLS_28 = {}.Search;
/** @type {[typeof __VLS_components.Search, ]} */ ;
/** @type {[typeof SearchBar, ]} */ ;
// @ts-ignore
const __VLS_29 = __VLS_asFunctionalComponent(__VLS_28, new __VLS_28({
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
}));
const __VLS_30 = __VLS_29({
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
}, ...__VLS_functionalComponentArgsRest(__VLS_29));
/** @type {[typeof Input, ]} */ ;
// @ts-ignore
const __VLS_32 = __VLS_asFunctionalComponent(Input, new Input({
modelValue: (__VLS_ctx.searchValue),
placeholder: "Search",
...{ class: "pl-8" },
}));
const __VLS_33 = __VLS_32({
modelValue: (__VLS_ctx.searchValue),
placeholder: "Search",
...{ class: "pl-8" },
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
const __VLS_35 = {}.TabsContent;
const __VLS_28 = __VLS_asFunctionalComponent(SearchBar, new SearchBar({}));
const __VLS_29 = __VLS_28({}, ...__VLS_functionalComponentArgsRest(__VLS_28));
const __VLS_31 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
const __VLS_32 = __VLS_asFunctionalComponent(__VLS_31, new __VLS_31({
value: "all",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_37 = __VLS_36({
const __VLS_33 = __VLS_32({
value: "all",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
__VLS_34.slots.default;
const __VLS_35 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
...{ class: "h-full" },
}));
const __VLS_37 = __VLS_36({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_36));
__VLS_38.slots.default;
const __VLS_39 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_39 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_40 = __VLS_asFunctionalComponent(__VLS_39, new __VLS_39({
...{ class: "h-full" },
@ -241,103 +224,129 @@ const __VLS_41 = __VLS_40({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_40));
__VLS_42.slots.default;
const __VLS_43 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
...{ class: "h-full" },
}));
const __VLS_45 = __VLS_44({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
__VLS_46.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_47 = {}.TransitionGroup;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_43 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_48 = __VLS_asFunctionalComponent(__VLS_47, new __VLS_47({
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
name: "list",
appear: true,
}));
const __VLS_49 = __VLS_48({
const __VLS_45 = __VLS_44({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_48));
__VLS_50.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
__VLS_46.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_51 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
const __VLS_47 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_52 = __VLS_51({
const __VLS_48 = __VLS_47({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_51));
let __VLS_54;
let __VLS_55;
let __VLS_56;
const __VLS_57 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_47));
let __VLS_50;
let __VLS_51;
let __VLS_52;
const __VLS_53 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_53;
var __VLS_49;
}
var __VLS_50;
var __VLS_46;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_46;
const __VLS_58 = {}.ScrollAreaScrollbar;
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_42;
const __VLS_54 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
const __VLS_55 = __VLS_asFunctionalComponent(__VLS_54, new __VLS_54({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_60 = __VLS_59({
const __VLS_56 = __VLS_55({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
__VLS_61.slots.default;
const __VLS_62 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_55));
__VLS_57.slots.default;
const __VLS_58 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_64 = __VLS_63({
const __VLS_60 = __VLS_59({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
var __VLS_61;
var __VLS_42;
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
var __VLS_57;
var __VLS_38;
const __VLS_66 = {}.TabsContent;
var __VLS_34;
const __VLS_62 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
value: "unread",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_68 = __VLS_67({
const __VLS_64 = __VLS_63({
value: "unread",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
__VLS_65.slots.default;
const __VLS_66 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
...{ class: "h-full" },
}));
const __VLS_68 = __VLS_67({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_67));
__VLS_69.slots.default;
const __VLS_70 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_70 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_71 = __VLS_asFunctionalComponent(__VLS_70, new __VLS_70({
...{ class: "h-full" },
@ -346,103 +355,129 @@ const __VLS_72 = __VLS_71({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_71));
__VLS_73.slots.default;
const __VLS_74 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
...{ class: "h-full" },
}));
const __VLS_76 = __VLS_75({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
__VLS_77.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_78 = {}.TransitionGroup;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_74 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_79 = __VLS_asFunctionalComponent(__VLS_78, new __VLS_78({
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
name: "list",
appear: true,
}));
const __VLS_80 = __VLS_79({
const __VLS_76 = __VLS_75({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_79));
__VLS_81.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
__VLS_77.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_82 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
const __VLS_78 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_83 = __VLS_82({
const __VLS_79 = __VLS_78({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_82));
let __VLS_85;
let __VLS_86;
let __VLS_87;
const __VLS_88 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_78));
let __VLS_81;
let __VLS_82;
let __VLS_83;
const __VLS_84 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_84;
var __VLS_80;
}
var __VLS_81;
var __VLS_77;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_77;
const __VLS_89 = {}.ScrollAreaScrollbar;
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_73;
const __VLS_85 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
const __VLS_86 = __VLS_asFunctionalComponent(__VLS_85, new __VLS_85({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_91 = __VLS_90({
const __VLS_87 = __VLS_86({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
__VLS_92.slots.default;
const __VLS_93 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_86));
__VLS_88.slots.default;
const __VLS_89 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_95 = __VLS_94({
const __VLS_91 = __VLS_90({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
var __VLS_92;
var __VLS_73;
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
var __VLS_88;
var __VLS_69;
const __VLS_97 = {}.TabsContent;
var __VLS_65;
const __VLS_93 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
value: "starred",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_99 = __VLS_98({
const __VLS_95 = __VLS_94({
value: "starred",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
__VLS_96.slots.default;
const __VLS_97 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
...{ class: "h-full" },
}));
const __VLS_99 = __VLS_98({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_98));
__VLS_100.slots.default;
const __VLS_101 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_101 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_102 = __VLS_asFunctionalComponent(__VLS_101, new __VLS_101({
...{ class: "h-full" },
@ -451,99 +486,115 @@ const __VLS_103 = __VLS_102({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_102));
__VLS_104.slots.default;
const __VLS_105 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
...{ class: "h-full" },
}));
const __VLS_107 = __VLS_106({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
__VLS_108.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_109 = {}.TransitionGroup;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_105 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_110 = __VLS_asFunctionalComponent(__VLS_109, new __VLS_109({
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
name: "list",
appear: true,
}));
const __VLS_111 = __VLS_110({
const __VLS_107 = __VLS_106({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_110));
__VLS_112.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
__VLS_108.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_113 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
const __VLS_109 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_114 = __VLS_113({
const __VLS_110 = __VLS_109({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_113));
let __VLS_116;
let __VLS_117;
let __VLS_118;
const __VLS_119 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_109));
let __VLS_112;
let __VLS_113;
let __VLS_114;
const __VLS_115 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_115;
var __VLS_111;
}
var __VLS_112;
var __VLS_108;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_108;
const __VLS_120 = {}.ScrollAreaScrollbar;
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_104;
const __VLS_116 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
const __VLS_117 = __VLS_asFunctionalComponent(__VLS_116, new __VLS_116({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_122 = __VLS_121({
const __VLS_118 = __VLS_117({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
__VLS_123.slots.default;
const __VLS_124 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_117));
__VLS_119.slots.default;
const __VLS_120 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_125 = __VLS_asFunctionalComponent(__VLS_124, new __VLS_124({
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_126 = __VLS_125({
const __VLS_122 = __VLS_121({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_125));
var __VLS_123;
var __VLS_104;
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
var __VLS_119;
var __VLS_100;
var __VLS_96;
var __VLS_3;
/** @type {[typeof Dialog, typeof Dialog, ]} */ ;
// @ts-ignore
const __VLS_128 = __VLS_asFunctionalComponent(Dialog, new Dialog({
const __VLS_124 = __VLS_asFunctionalComponent(Dialog, new Dialog({
open: (__VLS_ctx.showTrashConfirm),
}));
const __VLS_129 = __VLS_128({
const __VLS_125 = __VLS_124({
open: (__VLS_ctx.showTrashConfirm),
}, ...__VLS_functionalComponentArgsRest(__VLS_128));
__VLS_130.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_124));
__VLS_126.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "space-y-4" },
});
@ -558,43 +609,43 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
});
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_131 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_127 = __VLS_asFunctionalComponent(Button, new Button({
...{ 'onClick': {} },
variant: "outline",
}));
const __VLS_132 = __VLS_131({
const __VLS_128 = __VLS_127({
...{ 'onClick': {} },
variant: "outline",
}, ...__VLS_functionalComponentArgsRest(__VLS_131));
let __VLS_134;
let __VLS_135;
let __VLS_136;
const __VLS_137 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_127));
let __VLS_130;
let __VLS_131;
let __VLS_132;
const __VLS_133 = {
onClick: (...[$event]) => {
__VLS_ctx.showTrashConfirm = false;
}
};
__VLS_133.slots.default;
var __VLS_133;
__VLS_129.slots.default;
var __VLS_129;
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_138 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_134 = __VLS_asFunctionalComponent(Button, new Button({
...{ 'onClick': {} },
variant: "destructive",
}));
const __VLS_139 = __VLS_138({
const __VLS_135 = __VLS_134({
...{ 'onClick': {} },
variant: "destructive",
}, ...__VLS_functionalComponentArgsRest(__VLS_138));
let __VLS_141;
let __VLS_142;
let __VLS_143;
const __VLS_144 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_134));
let __VLS_137;
let __VLS_138;
let __VLS_139;
const __VLS_140 = {
onClick: (__VLS_ctx.performEmptyTrash)
};
__VLS_140.slots.default;
var __VLS_140;
var __VLS_130;
__VLS_136.slots.default;
var __VLS_136;
var __VLS_126;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
@ -697,13 +748,6 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['backdrop-blur']} */ ;
/** @type {__VLS_StyleScopedClasses['supports-[backdrop-filter]:bg-background/60']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['left-2']} */ ;
/** @type {__VLS_StyleScopedClasses['top-2.5']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['pl-8']} */ ;
/** @type {__VLS_StyleScopedClasses['m-0']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
@ -714,9 +758,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -738,9 +793,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -762,9 +828,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -791,7 +868,6 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
Search: Search,
ArrowDownUp: ArrowDownUp,
Trash2: Trash2,
TabsRoot: TabsRoot,
@ -803,11 +879,10 @@ const __VLS_self = (await import('vue')).defineComponent({
ScrollAreaScrollbar: ScrollAreaScrollbar,
ScrollAreaThumb: ScrollAreaThumb,
MessageListItem: MessageListItem,
Input: Input,
SearchBar: SearchBar,
Dialog: Dialog,
Button: Button,
store: store,
searchValue: searchValue,
showTrashConfirm: showTrashConfirm,
folderColor: folderColor,
displayMessages: displayMessages,

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ import { Paperclip, Star } from 'lucide-vue-next'
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue'
import { cn } from '../../lib/utils'
import Badge from '../ui/Badge.vue'
import { useMailStore } from '../../stores/mail'
const props = defineProps({
message: { type: Object, required: true },
@ -12,6 +13,8 @@ const props = defineProps({
folderColor: { type: String, default: '' },
})
const store = useMailStore()
const timestamp = computed(() => props.message.received_at || props.message.sent_at)
const relativeTime = computed(() =>
timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : ''
@ -19,6 +22,22 @@ const relativeTime = computed(() =>
const exactTime = computed(() =>
timestamp.value ? format(new Date(timestamp.value), 'PPpp') : ''
)
const isSentOrDrafts = computed(() => {
const name = store.currentFolderObj?.name?.toLowerCase() ?? ''
return name === 'sent' || name === 'drafts'
})
const recipientLabel = computed(() => {
if (!isSentOrDrafts.value) return null
let addrs: string[] = []
try { addrs = JSON.parse(props.message.to_addresses || '[]') } catch { addrs = [] }
if (!addrs.length) return null
return 'To: ' + addrs.map((a: string) => {
const m = a.match(/<([^>]+)>/)
return m ? m[1] : a
}).join(', ')
})
</script>
<template>
@ -32,7 +51,7 @@ const exactTime = computed(() =>
<div class="flex w-full flex-col gap-1">
<div class="flex items-center">
<div class="flex items-center gap-2">
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
<div class="font-semibold">{{ recipientLabel ?? (message.from_name || message.from_address) }}</div>
<span v-if="!message.is_read" class="flex h-2 w-2 rounded-full bg-primary" />
<Star v-if="message.is_starred" class="size-3 fill-yellow-400 text-yellow-400" />
</div>

View file

@ -5,14 +5,37 @@ import { Paperclip, Star } from 'lucide-vue-next';
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue';
import { cn } from '../../lib/utils';
import Badge from '../ui/Badge.vue';
import { useMailStore } from '../../stores/mail';
const props = defineProps({
message: { type: Object, required: true },
selected: { type: Boolean, default: false },
folderColor: { type: String, default: '' },
});
const store = useMailStore();
const timestamp = computed(() => props.message.received_at || props.message.sent_at);
const relativeTime = computed(() => timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : '');
const exactTime = computed(() => timestamp.value ? format(new Date(timestamp.value), 'PPpp') : '');
const isSentOrDrafts = computed(() => {
const name = store.currentFolderObj?.name?.toLowerCase() ?? '';
return name === 'sent' || name === 'drafts';
});
const recipientLabel = computed(() => {
if (!isSentOrDrafts.value)
return null;
let addrs = [];
try {
addrs = JSON.parse(props.message.to_addresses || '[]');
}
catch {
addrs = [];
}
if (!addrs.length)
return null;
return 'To: ' + addrs.map((a) => {
const m = a.match(/<([^>]+)>/);
return m ? m[1] : a;
}).join(', ');
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
@ -33,7 +56,7 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "font-semibold" },
});
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
(__VLS_ctx.recipientLabel ?? (__VLS_ctx.message.from_name || __VLS_ctx.message.from_address));
if (!__VLS_ctx.message.is_read) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
...{ class: "flex h-2 w-2 rounded-full bg-primary" },
@ -183,6 +206,7 @@ const __VLS_self = (await import('vue')).defineComponent({
timestamp: timestamp,
relativeTime: relativeTime,
exactTime: exactTime,
recipientLabel: recipientLabel,
};
},
props: {

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@ const store = useMailStore()
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="store.isSettingsOpen = false"
>
<div class="relative w-full max-w-3xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
<div class="relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
<button
class="absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
@click="store.isSettingsOpen = false"

View file

@ -0,0 +1,115 @@
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { X } from 'lucide-vue-next';
import { useMailStore } from '../../stores/mail';
import SettingsContent from '../settings/SettingsContent.vue';
const store = useMailStore();
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
// CSS variable injection
// CSS variable injection end
const __VLS_0 = {}.Teleport;
/** @type {[typeof __VLS_components.Teleport, typeof __VLS_components.Teleport, ]} */ ;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({
to: "body",
}));
const __VLS_2 = __VLS_1({
to: "body",
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
__VLS_3.slots.default;
const __VLS_4 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_5 = __VLS_asFunctionalComponent(__VLS_4, new __VLS_4({
name: "fade",
}));
const __VLS_6 = __VLS_5({
name: "fade",
}, ...__VLS_functionalComponentArgsRest(__VLS_5));
__VLS_7.slots.default;
if (__VLS_ctx.store.isSettingsOpen) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.store.isSettingsOpen))
return;
__VLS_ctx.store.isSettingsOpen = false;
} },
...{ class: "fixed inset-0 z-50 flex items-center justify-center bg-black/50" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.store.isSettingsOpen))
return;
__VLS_ctx.store.isSettingsOpen = false;
} },
...{ class: "absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" },
});
const __VLS_8 = {}.X;
/** @type {[typeof __VLS_components.X, ]} */ ;
// @ts-ignore
const __VLS_9 = __VLS_asFunctionalComponent(__VLS_8, new __VLS_8({
...{ class: "size-4" },
}));
const __VLS_10 = __VLS_9({
...{ class: "size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
/** @type {[typeof SettingsContent, ]} */ ;
// @ts-ignore
const __VLS_12 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
const __VLS_13 = __VLS_12({}, ...__VLS_functionalComponentArgsRest(__VLS_12));
}
var __VLS_7;
var __VLS_3;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/50']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-[90vh]']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-y-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
/** @type {__VLS_StyleScopedClasses['top-4']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['h-7']} */ ;
/** @type {__VLS_StyleScopedClasses['w-7']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-accent-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
X: X,
SettingsContent: SettingsContent,
store: store,
};
},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
});
; /* PartiallyEnd: #4569/main.vue */
//# sourceMappingURL=SettingsDialog.vue.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"SettingsDialog.vue.js","sourceRoot":"","sources":["SettingsDialog.vue"],"names":[],"mappings":"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"}

View file

@ -6,6 +6,7 @@ import { usePushSubscription } from '@/composables/usePushSubscription'
import { useMailStore } from '@/stores/mail'
import { mailApi } from '@/api/mail'
import { authApi } from '@/api/auth'
import type { Alias } from '@/types/mail'
const { canInstall, promptInstall } = useInstallPrompt()
const notificationsStore = useNotificationsStore()
@ -167,6 +168,98 @@ async function deleteAutoResponder() {
arDeleteLoading.value = false
}
}
const alAliases = ref<Alias[]>([])
const alLoading = ref(false)
const alError = ref('')
const alSuccess = ref('')
const alNewAddress = ref('')
const alNewLabel = ref('')
const alNewDescription = ref('')
const alNewExpires = ref('')
const alNeverExpire = ref(true)
const alCreateLoading = ref(false)
const alGenerating = ref(false)
async function loadAliases(address: string) {
alLoading.value = true
alError.value = ''
try {
const res = await mailApi.getAllAliases(address)
alAliases.value = res.data.aliases || []
} catch {
alError.value = 'Failed to load aliases.'
} finally {
alLoading.value = false
}
}
watch(
() => mailStore.currentMailbox,
(address) => { if (address) loadAliases(address) },
{ immediate: true }
)
async function generateAlias() {
if (!mailStore.currentMailbox) return
const domain = mailStore.currentMailbox.split('@')[1]
if (!domain) return
alGenerating.value = true
try {
const res = await mailApi.generateAlias(mailStore.currentMailbox, domain)
alNewAddress.value = res.data.suggestion?.split('@')[0] || ''
} catch { /* ignore */ } finally {
alGenerating.value = false
}
}
async function createAlias() {
if (!mailStore.currentMailbox || !alNewAddress.value) return
const domain = mailStore.currentMailbox.split('@')[1]
alError.value = ''
alSuccess.value = ''
alCreateLoading.value = true
try {
await mailApi.createAlias({
address: `${alNewAddress.value}@${domain}`,
mailbox_address: mailStore.currentMailbox,
label: alNewLabel.value || undefined,
description: alNewDescription.value || undefined,
expires_at: alNeverExpire.value ? null : (alNewExpires.value || null),
})
alNewAddress.value = ''
alNewLabel.value = ''
alNewDescription.value = ''
alNewExpires.value = ''
alNeverExpire.value = true
alSuccess.value = 'Alias created.'
await loadAliases(mailStore.currentMailbox)
} catch (e: any) {
alError.value = e?.response?.data?.error || 'Failed to create alias.'
} finally {
alCreateLoading.value = false
}
}
async function toggleAlias(alias: Alias) {
try {
await mailApi.updateAlias(alias.address, { is_active: !alias.is_active })
alias.is_active = !alias.is_active
} catch { /* ignore */ }
}
async function deleteAlias(alias: Alias) {
if (!confirm(`Delete alias ${alias.address}? This cannot be undone.`)) return
try {
await mailApi.deleteAlias(alias.address)
alAliases.value = alAliases.value.filter(a => a.address !== alias.address)
} catch { /* ignore */ }
}
function formatDate(iso: string | null) {
if (!iso) return '—'
return new Date(iso).toLocaleDateString()
}
</script>
<template>
@ -410,6 +503,111 @@ async function deleteAutoResponder() {
</button>
</div>
<div v-if="mailStore.currentMailbox" class="mb-4 rounded-lg border p-4 space-y-3">
<div>
<h2 class="text-sm font-medium">Email Aliases</h2>
<p class="text-sm text-muted-foreground mt-0.5">
Create aliases that forward mail to your mailbox while keeping your real address private.
</p>
</div>
<div v-if="alLoading" class="text-sm text-muted-foreground">Loading</div>
<template v-else>
<div v-if="alAliases.length" class="divide-y divide-border rounded-md border overflow-hidden">
<div
v-for="alias in alAliases"
:key="alias.address"
class="flex items-center gap-3 px-3 py-2.5 bg-background text-sm"
>
<div class="flex-1 min-w-0">
<div class="font-mono text-xs truncate">{{ alias.address }}</div>
<div class="flex items-center gap-2 mt-0.5">
<span v-if="alias.label" class="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ alias.label }}</span>
<span v-if="alias.expires_at" class="text-xs text-muted-foreground">Expires {{ formatDate(alias.expires_at) }}</span>
<span class="text-xs text-muted-foreground">{{ alias.use_count }} received</span>
</div>
</div>
<button
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200"
:class="alias.is_active ? 'bg-primary' : 'bg-muted'"
role="switch"
:aria-checked="alias.is_active"
@click="toggleAlias(alias)"
>
<span
class="pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform duration-200"
:class="alias.is_active ? 'translate-x-4' : 'translate-x-0'"
/>
</button>
<button
class="text-muted-foreground hover:text-destructive transition-colors text-xs"
@click="deleteAlias(alias)"
>
Delete
</button>
</div>
</div>
<p v-else class="text-sm text-muted-foreground">No aliases yet.</p>
<div class="space-y-2 pt-1">
<label class="text-sm font-medium">Create alias</label>
<div class="flex items-center gap-2">
<input
v-model="alNewAddress"
type="text"
placeholder="local-part"
class="flex-1 rounded-md border bg-background px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-ring"
/>
<span class="text-sm text-muted-foreground shrink-0">@{{ mailStore.currentMailbox.split('@')[1] }}</span>
<button
:disabled="alGenerating"
class="inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50 shrink-0"
@click="generateAlias"
>
{{ alGenerating ? '…' : 'Generate' }}
</button>
</div>
<input
v-model="alNewLabel"
type="text"
placeholder="Label (optional, e.g. shopping)"
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<input
v-model="alNewDescription"
type="text"
placeholder="Description (optional)"
maxlength="200"
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<div class="flex items-center gap-3">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input v-model="alNeverExpire" type="checkbox" class="rounded border-border" />
Never expire
</label>
<input
v-if="!alNeverExpire"
v-model="alNewExpires"
type="date"
class="rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
</div>
<p v-if="alError" class="text-xs text-destructive">{{ alError }}</p>
<p v-if="alSuccess" class="text-xs text-green-600">{{ alSuccess }}</p>
<button
:disabled="alCreateLoading || !alNewAddress"
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
@click="createAlias"
>
{{ alCreateLoading ? 'Creating…' : 'Create Alias' }}
</button>
</template>
</div>
<p class="text-sm text-muted-foreground">Additional settings are managed in DockFlare Master.</p>
</div>
</template>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
import { ref, onMounted, onUnmounted } from 'vue';
const isMobile = ref(false);
export function useBreakpoint() {
const mq = window.matchMedia('(max-width: 767px)');
const update = (e) => {
isMobile.value = e.matches;
};
onMounted(() => {
isMobile.value = mq.matches;
mq.addEventListener('change', update);
});
onUnmounted(() => mq.removeEventListener('change', update));
return { isMobile };
}
//# sourceMappingURL=useBreakpoint.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"useBreakpoint.js","sourceRoot":"","sources":["useBreakpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAEjD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;AAE3B,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAA;IAElD,MAAM,MAAM,GAAG,CAAC,CAAuC,EAAE,EAAE;QACzD,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAA;IAC5B,CAAC,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,KAAK,GAAG,EAAE,CAAC,OAAO,CAAA;QAC3B,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;IAE3D,OAAO,EAAE,QAAQ,EAAE,CAAA;AACrB,CAAC"}

View file

@ -0,0 +1,20 @@
import { ref, onMounted, onUnmounted } from 'vue'
const isMobile = ref(false)
export function useBreakpoint() {
const mq = window.matchMedia('(max-width: 767px)')
const update = (e: MediaQueryListEvent | MediaQueryList) => {
isMobile.value = e.matches
}
onMounted(() => {
isMobile.value = mq.matches
mq.addEventListener('change', update)
})
onUnmounted(() => mq.removeEventListener('change', update))
return { isMobile }
}

View file

@ -1,18 +1,41 @@
import { onUnmounted, ref, watch } from 'vue';
import { mailApi } from '@/api/mail';
import { useNotificationsStore } from '@/stores/notifications';
import { useMailStore } from '@/stores/mail';
function updateBadge(count) {
if (!('setAppBadge' in navigator))
return;
if (count > 0) {
navigator.setAppBadge(count).catch(() => { });
}
else {
navigator.clearAppBadge().catch(() => { });
}
}
async function getPushIntervalMs() {
try {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
if (sub)
return 300_000;
}
}
catch { /* SW not available */ }
return 30_000 + Math.random() * 20_000 - 10_000;
}
export function useMailPolling() {
const notificationsStore = useNotificationsStore();
const mailStore = useMailStore();
const lastSeen = ref({});
const initialized = ref(false);
let intervalId = null;
const poll = async () => {
if (mailStore.mailboxes.length === 0)
return;
try {
const res = await mailApi.getMailboxStatus();
const statuses = res.data;
const totalUnread = statuses.reduce((sum, s) => sum + s.unread_count, 0);
updateBadge(totalUnread);
if (!initialized.value) {
for (const s of statuses) {
lastSeen.value[s.address] = s.latest_received_at;
@ -20,17 +43,39 @@ export function useMailPolling() {
initialized.value = true;
return;
}
if (!notificationsStore.isGranted)
return;
for (const s of statuses) {
const prev = lastSeen.value[s.address];
if (s.latest_received_at &&
(prev === undefined || prev === null || s.latest_received_at > prev)) {
lastSeen.value[s.address] = s.latest_received_at;
if (s.address === mailStore.currentMailbox && mailStore.currentFolder) {
try {
const mRes = await mailApi.getMessages(s.address, {
folder: mailStore.currentFolder,
order: mailStore.sortOrder,
page: 1,
per_page: 50,
});
const payload = mRes.data;
const items = Array.isArray(payload) ? payload : payload.items || [];
mailStore.messages = items;
mailStore.totalMessages = payload.total ?? items.length;
mailStore.messagesPage = 1;
mailStore.hasMoreMessages = items.length === 50;
}
catch { /* network error — skip */ }
try {
const fRes = await mailApi.getFolders(s.address);
mailStore.folders = fRes.data;
}
catch { /* network error — skip */ }
}
if (Notification.permission === 'granted') {
fireNotification(s.address, s.unread_count);
}
}
}
}
catch {
// network error — skip
}
@ -48,11 +93,34 @@ export function useMailPolling() {
n.close();
};
};
const startInterval = async () => {
if (intervalId)
clearInterval(intervalId);
const ms = await getPushIntervalMs();
intervalId = setInterval(poll, ms);
};
const onVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
else {
poll();
startInterval();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
watch(() => mailStore.mailboxes, (boxes) => {
if (boxes.length > 0 && !initialized.value)
poll();
}, { immediate: true });
const interval = setInterval(poll, 60_000);
onUnmounted(() => clearInterval(interval));
startInterval();
onUnmounted(() => {
if (intervalId)
clearInterval(intervalId);
document.removeEventListener('visibilitychange', onVisibilityChange);
});
}
//# sourceMappingURL=useMailPolling.js.map

View file

@ -1 +1 @@
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}

View file

@ -1,5 +1,6 @@
import { ref } from 'vue';
import apiClient from '@/api/client';
import { useMailStore } from '@/stores/mail';
const isSupported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window;
function urlBase64ToUint8Array(base64) {
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
@ -12,6 +13,8 @@ function urlBase64ToUint8Array(base64) {
export function usePushSubscription() {
const isSubscribed = ref(false);
const isLoading = ref(false);
const error = ref(null);
const mailStore = useMailStore();
const checkSubscription = async () => {
if (!isSupported)
return;
@ -19,10 +22,11 @@ export function usePushSubscription() {
const sub = await reg.pushManager.getSubscription();
isSubscribed.value = !!sub;
};
const subscribe = async (mailboxAddress) => {
const subscribe = async () => {
if (!isSupported)
return;
isLoading.value = true;
error.value = null;
try {
const { data } = await apiClient.get('/notifications/vapid-key');
const reg = await navigator.serviceWorker.ready;
@ -31,13 +35,28 @@ export function usePushSubscription() {
applicationServerKey: urlBase64ToUint8Array(data.public_key),
});
const subJson = sub.toJSON();
let addresses = mailStore.mailboxes.map((m) => m.address);
if (addresses.length === 0) {
const statusRes = await apiClient.get('/mailboxes/status');
addresses = statusRes.data.map((s) => s.address);
}
if (addresses.length === 0) {
error.value = 'No mailboxes found';
return;
}
for (const address of addresses) {
await apiClient.post('/notifications/subscribe', {
endpoint: subJson.endpoint,
keys: subJson.keys,
mailbox_address: mailboxAddress,
mailbox_address: address,
});
}
isSubscribed.value = true;
}
catch (err) {
console.error('Push subscribe failed:', err);
error.value = err?.message ?? 'Subscription failed';
}
finally {
isLoading.value = false;
}
@ -46,6 +65,7 @@ export function usePushSubscription() {
if (!isSupported)
return;
isLoading.value = true;
error.value = null;
try {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
@ -57,11 +77,15 @@ export function usePushSubscription() {
}
isSubscribed.value = false;
}
catch (err) {
console.error('Push unsubscribe failed:', err);
error.value = err?.message ?? 'Unsubscribe failed';
}
finally {
isLoading.value = false;
}
};
checkSubscription();
return { isSubscribed, isLoading, isSupported, subscribe, unsubscribe };
return { isSubscribed, isLoading, isSupported, error, subscribe, unsubscribe };
}
//# sourceMappingURL=usePushSubscription.js.map

View file

@ -1 +1 @@
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}

View file

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { setIDBItem, removeIDBItem } from '@/lib/idb';
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('jwt_token') || '');
const isAuthenticated = computed(() => {
@ -16,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
const setToken = (newToken) => {
token.value = newToken;
localStorage.setItem('jwt_token', newToken);
setIDBItem('jwt_token', newToken).catch(() => { });
};
const logout = async () => {
if ('serviceWorker' in navigator) {
@ -38,6 +40,7 @@ export const useAuthStore = defineStore('auth', () => {
}
token.value = '';
localStorage.removeItem('jwt_token');
removeIDBItem('jwt_token').catch(() => { });
};
const decodeToken = () => {
if (!token.value)

View file

@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAA;IAE1D,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE;QACpC,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAC9B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAG,CAAC,QAAgB,EAAE,EAAE;QACpC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAA;QACtB,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;QACxB,IAAI,eAAe,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;gBAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,eAAe,EAAE,CAAA;gBACnD,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,KAAK,CAAC,iCAAiC,EAAE;wBAC7C,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE;4BACP,cAAc,EAAE,kBAAkB;4BAClC,eAAe,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE;yBACzC;wBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;qBACjD,CAAC,CAAA;oBACF,MAAM,GAAG,CAAC,WAAW,EAAE,CAAA;gBACzB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,gCAAgC,CAAC,CAAC;QAC9C,CAAC;QACD,KAAK,CAAC,KAAK,GAAG,EAAE,CAAA;QAChB,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IACtC,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YACzC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;AAClE,CAAC,CAAC,CAAA"}
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}

View file

@ -1,14 +1,20 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { ref, shallowRef, computed } from 'vue';
export const useMailStore = defineStore('mail', () => {
const mailboxes = ref([]);
const currentMailbox = ref('');
const folders = ref([]);
const currentFolder = ref('');
const messages = ref([]);
const messages = shallowRef([]);
const totalMessages = ref(0);
const hasMoreMessages = ref(false);
const messagesPage = ref(1);
const isFetchingNextPage = ref(false);
const currentMessage = ref(null);
const messagesLoading = ref(false);
const isComposeOpen = ref(false);
const isComposeFullView = ref(false);
const isSettingsOpen = ref(false);
const composeDefaults = ref(null);
const composeBody = ref('');
const activeTab = ref('all');
@ -16,6 +22,22 @@ export const useMailStore = defineStore('mail', () => {
const sortOrder = ref('desc');
const isDark = ref(localStorage.getItem('theme') === 'dark');
const viewMode = ref(localStorage.getItem('viewMode') || 'split');
const toast = ref(null);
let toastTimer = null;
let _loadMore = null;
function showToast(message, type = 'error') {
if (toastTimer)
clearTimeout(toastTimer);
toast.value = { message, type };
toastTimer = setTimeout(() => { toast.value = null; }, 4000);
}
function registerLoadMore(fn) {
_loadMore = fn;
}
function loadMore() {
if (_loadMore)
_loadMore();
}
const unreadMessages = computed(() => messages.value.filter((m) => !m.is_read));
const starredMessages = computed(() => messages.value.filter((m) => m.is_starred));
const currentFolderObj = computed(() => folders.value.find((f) => f.name === currentFolder.value) || null);
@ -37,12 +59,15 @@ export const useMailStore = defineStore('mail', () => {
return {
mailboxes, currentMailbox,
folders, currentFolder, currentFolderObj,
messages, currentMessage,
isComposeOpen, isComposeFullView, composeDefaults, composeBody,
messages, totalMessages, hasMoreMessages, messagesPage, isFetchingNextPage,
currentMessage, messagesLoading,
isComposeOpen, isComposeFullView, isSettingsOpen, composeDefaults, composeBody,
activeTab, isCollapsed,
sortOrder, isDark, toggleTheme,
viewMode, toggleViewMode,
unreadMessages, starredMessages,
toast, showToast,
registerLoadMore, loadMore,
};
});
//# sourceMappingURL=mail.js.map

View file

@ -1 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAG/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE;IACnD,MAAM,SAAS,GAAG,GAAG,CAAY,EAAE,CAAC,CAAA;IACpC,MAAM,cAAc,GAAG,GAAG,CAAS,EAAE,CAAC,CAAA;IACtC,MAAM,OAAO,GAAG,GAAG,CAAW,EAAE,CAAC,CAAA;IACjC,MAAM,aAAa,GAAG,GAAG,CAAS,EAAE,CAAC,CAAA;IACrC,MAAM,QAAQ,GAAG,UAAU,CAAY,EAAE,CAAC,CAAA;IAC1C,MAAM,aAAa,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IAC5B,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,YAAY,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,kBAAkB,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACrC,MAAM,cAAc,GAAG,GAAG,CAAiB,IAAI,CAAC,CAAA;IAChD,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAChC,MAAM,iBAAiB,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACjC,MAAM,eAAe,GAAG,GAAG,CAAyB,IAAI,CAAC,CAAA;IACzD,MAAM,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC,CAAA;IAC3B,MAAM,SAAS,GAAG,GAAG,CAA+B,KAAK,CAAC,CAAA;IAC1D,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC9B,MAAM,SAAS,GAAG,GAAG,CAAiB,MAAM,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,CAAA;IAC5D,MAAM,QAAQ,GAAG,GAAG,CAAoB,YAAY,CAAC,OAAO,CAAC,UAAU,CAAsB,IAAI,OAAO,CAAC,CAAA;IACzG,MAAM,KAAK,GAAG,GAAG,CAAe,IAAI,CAAC,CAAA;IAErC,IAAI,UAAU,GAAyC,IAAI,CAAA;IAC3D,IAAI,SAAS,GAAwB,IAAI,CAAA;IAEzC,SAAS,SAAS,CAAC,OAAe,EAAE,OAAsB,OAAO;QAC/D,IAAI,UAAU;YAAE,YAAY,CAAC,UAAU,CAAC,CAAA;QACxC,KAAK,CAAC,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;QAC/B,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,IAAI,CAAA,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAC7D,CAAC;IAED,SAAS,gBAAgB,CAAC,EAAc;QACtC,SAAS,GAAG,EAAE,CAAA;IAChB,CAAC;IAED,SAAS,QAAQ;QACf,IAAI,SAAS;YAAE,SAAS,EAAE,CAAA;IAC5B,CAAC;IAED,MAAM,cAAc,GAAG,QAAQ,CAAC,GAAG,EAAE,CACnC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CACzC,CAAA;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE,CACpC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAC3C,CAAA;IAED,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,EAAE,CACrC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,KAAK,CAAC,IAAI,IAAI,CAClE,CAAA;IAED,SAAS,WAAW;QAClB,MAAM,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,KAAK,CAAA;QAC5B,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC9C,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACvC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACjD,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,SAAS,cAAc;QACrB,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;QAC9D,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;IAClD,CAAC;IAED,OAAO;QACL,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,aAAa,EAAE,gBAAgB;QACxC,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB;QAC1E,cAAc,EAAE,eAAe;QAC/B,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW;QAC9E,SAAS,EAAE,WAAW;QACtB,SAAS,EAAE,MAAM,EAAE,WAAW;QAC9B,QAAQ,EAAE,cAAc;QACxB,cAAc,EAAE,eAAe;QAC/B,KAAK,EAAE,SAAS;QAChB,gBAAgB,EAAE,QAAQ;KAC3B,CAAA;AACH,CAAC,CAAC,CAAA"}

View file

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=mail.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":""}

View file

@ -28,6 +28,7 @@ export interface Message {
from_address: string
to_addresses: string
cc_addresses: string
bcc_addresses: string
subject: string | null
text_body: string | null
html_body: string | null
@ -36,9 +37,23 @@ export interface Message {
is_read: 0 | 1
is_starred: 0 | 1
is_draft: boolean
received_via_alias: string | null
attachments?: Attachment[]
}
export interface Alias {
address: string
mailbox_address: string
domain: string
label: string | null
description: string | null
is_active: boolean
expires_at: string | null
created_at: string
use_count: number
last_use_at: string | null
}
export interface Toast {
message: string
type: 'error' | 'success' | 'info'
@ -46,6 +61,7 @@ export interface Toast {
export interface ComposeDefaults {
to?: string
from?: string
subject?: string
body?: string
quotedHtml?: string

View file

@ -1,26 +1,65 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { onMounted, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useMail } from '../composables/useMail';
import { useMailPolling } from '../composables/useMailPolling';
import { useNotificationsStore } from '../stores/notifications';
import { mailApi } from '../api/mail';
import MailLayout from '../components/mail/MailLayout.vue';
const route = useRoute();
const { store, loadMailboxes } = useMail();
const mailStore = store;
const notificationsStore = useNotificationsStore();
useMailPolling();
const loadMessages = async (addr, folder) => {
const showNotifPrompt = ref(false);
let mailboxLoadSeq = 0;
const loadMessages = async (addr, folder, page = 1) => {
if (!addr || !folder)
return;
if (page === 1) {
store.messagesLoading = true;
store.messages = [];
store.messagesPage = 1;
}
else {
store.isFetchingNextPage = true;
}
try {
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder });
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder, page, per_page: 50 });
const payload = mRes.data;
store.messages = Array.isArray(payload) ? payload : payload.items || [];
const items = Array.isArray(payload) ? payload : payload.items || [];
store.messages = page === 1 ? items : [...store.messages, ...items];
store.totalMessages = payload.total ?? items.length;
store.messagesPage = page;
store.hasMoreMessages = items.length === 50;
if (page === 1)
store.currentMessage = null;
}
catch (e) {
console.error('Failed to load messages', e);
catch {
store.showToast('Failed to load messages');
}
finally {
store.messagesLoading = false;
store.isFetchingNextPage = false;
}
};
store.registerLoadMore(() => {
if (store.hasMoreMessages && !store.isFetchingNextPage) {
loadMessages(store.currentMailbox, store.currentFolder, store.messagesPage + 1);
}
});
async function enableNotifications() {
await notificationsStore.requestPermission();
showNotifPrompt.value = false;
localStorage.setItem('notif_prompted', '1');
if (notificationsStore.isGranted) {
mailStore.isSettingsOpen = true;
}
}
function dismissPrompt() {
showNotifPrompt.value = false;
localStorage.setItem('notif_prompted', '1');
}
onMounted(async () => {
await loadMailboxes();
const mailboxParam = route.query.mailbox;
@ -29,27 +68,46 @@ onMounted(async () => {
if (found)
store.currentMailbox = mailboxParam;
}
if (Notification.permission === 'default' && !localStorage.getItem('notif_prompted')) {
showNotifPrompt.value = true;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (ev) => {
if (ev.data?.type === 'NOTIFICATION_CLICK' && ev.data.mailbox) {
store.currentMailbox = ev.data.mailbox;
}
if (ev.data?.type === 'SET_BADGE') {
const count = ev.data.count ?? 0;
if ('setAppBadge' in navigator) {
if (count > 0) {
navigator.setAppBadge(count).catch(() => { });
}
else {
navigator.clearAppBadge().catch(() => { });
}
}
}
});
}
});
watch(() => store.currentMailbox, async (addr) => {
if (!addr)
return;
const seq = ++mailboxLoadSeq;
try {
const fRes = await mailApi.getFolders(addr);
if (seq !== mailboxLoadSeq)
return;
store.folders = fRes.data;
if (store.folders.length > 0) {
const inbox = store.folders.find((f) => f.name.toLowerCase() === 'inbox');
store.currentFolder = inbox ? inbox.name : store.folders[0].name;
}
}
catch (e) {
console.error('Failed to load folders', e);
catch {
if (seq !== mailboxLoadSeq)
return;
store.showToast('Failed to load folders');
}
});
watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
@ -58,44 +116,154 @@ watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
watch(() => store.sortOrder, () => {
loadMessages(store.currentMailbox, store.currentFolder);
});
let openedMessageId = null;
watch(() => store.currentMessage, async (msg) => {
if (!msg || msg.attachments !== undefined)
if (!msg)
return;
try {
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
const fullMsg = res.data;
store.currentMessage = fullMsg;
const idx = store.messages.findIndex((m) => m.id === msg.id);
if (idx !== -1) {
let fullMsg = msg;
const isUserOpen = msg.attachments === undefined || msg.id !== openedMessageId;
if (msg.attachments === undefined) {
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
fullMsg = res.data;
store.currentMessage = fullMsg;
if (idx !== -1)
store.messages[idx] = fullMsg;
}
if (!fullMsg.is_read) {
if (!fullMsg.is_read && isUserOpen) {
openedMessageId = msg.id;
await mailApi.updateMessage(store.currentMailbox, msg.id, { is_read: true });
if (idx !== -1) {
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
}
store.currentMessage = { ...store.currentMessage, is_read: 1 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
else {
openedMessageId = msg.id;
}
}
catch (e) {
console.error('Failed to load message', e);
catch {
store.showToast('Failed to load message');
}
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative h-full" },
});
/** @type {[typeof MailLayout, ]} */ ;
// @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(MailLayout, new MailLayout({}));
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
var __VLS_3 = {};
var __VLS_2;
const __VLS_3 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_4 = __VLS_asFunctionalComponent(__VLS_3, new __VLS_3({
name: "slide-up",
}));
const __VLS_5 = __VLS_4({
name: "slide-up",
}, ...__VLS_functionalComponentArgsRest(__VLS_4));
__VLS_6.slots.default;
if (__VLS_ctx.showNotifPrompt) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 rounded-xl border bg-background shadow-lg px-5 py-3.5 text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-muted-foreground" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.enableNotifications) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.dismissPrompt) },
...{ class: "text-muted-foreground hover:text-foreground transition-colors" },
});
}
var __VLS_6;
const __VLS_7 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent(__VLS_7, new __VLS_7({
name: "slide-up",
}));
const __VLS_9 = __VLS_8({
name: "slide-up",
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
__VLS_10.slots.default;
if (__VLS_ctx.store.toast) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-xl border px-5 py-3.5 text-sm shadow-lg" },
...{ class: (__VLS_ctx.store.toast.type === 'error'
? 'bg-destructive text-destructive-foreground border-destructive'
: __VLS_ctx.store.toast.type === 'success'
? 'bg-green-600 text-white border-green-700'
: 'bg-background text-foreground border-border') },
});
(__VLS_ctx.store.toast.message);
}
var __VLS_10;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
/** @type {__VLS_StyleScopedClasses['left-1/2']} */ ;
/** @type {__VLS_StyleScopedClasses['-translate-x-1/2']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
MailLayout: MailLayout,
store: store,
showNotifPrompt: showNotifPrompt,
enableNotifications: enableNotifications,
dismissPrompt: dismissPrompt,
};
},
});

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,5 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { useInstallPrompt } from '@/composables/useInstallPrompt';
import { useNotificationsStore } from '@/stores/notifications';
import { usePushSubscription } from '@/composables/usePushSubscription';
import { useMailStore } from '@/stores/mail';
const { canInstall, promptInstall } = useInstallPrompt();
const notificationsStore = useNotificationsStore();
const push = usePushSubscription();
const mailStore = useMailStore();
import SettingsContent from '@/components/settings/SettingsContent.vue';
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
@ -14,197 +7,17 @@ let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 max-w-2xl" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-semibold mb-6" },
});
if (__VLS_ctx.canInstall) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-sm font-medium" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground mt-0.5" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.promptInstall) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-sm font-medium" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground mt-0.5" },
});
if (__VLS_ctx.notificationsStore.isDenied) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
}
else if (!__VLS_ctx.notificationsStore.isGranted) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.notificationsStore.requestPermission) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
if (__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-sm" },
});
(__VLS_ctx.mailStore.currentMailbox);
if (!__VLS_ctx.push.isSubscribed.value) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.notificationsStore.isDenied))
return;
if (!!(!__VLS_ctx.notificationsStore.isGranted))
return;
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
return;
if (!(!__VLS_ctx.push.isSubscribed.value))
return;
__VLS_ctx.push.subscribe(__VLS_ctx.mailStore.currentMailbox);
} },
disabled: (__VLS_ctx.push.isLoading.value),
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.notificationsStore.isDenied))
return;
if (!!(!__VLS_ctx.notificationsStore.isGranted))
return;
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
return;
if (!!(!__VLS_ctx.push.isSubscribed.value))
return;
__VLS_ctx.push.unsubscribe();
} },
disabled: (__VLS_ctx.push.isLoading.value),
...{ class: "inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50" },
});
}
}
else if (!__VLS_ctx.push.isSupported) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
/** @type {[typeof SettingsContent, ]} */ ;
// @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
canInstall: canInstall,
promptInstall: promptInstall,
notificationsStore: notificationsStore,
push: push,
mailStore: mailStore,
SettingsContent: SettingsContent,
};
},
});

File diff suppressed because one or more lines are too long