ultisuite-client/lib/demo/demo-mail-api-data.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

277 lines
7.7 KiB
TypeScript

import {
DEMO_EMAILS,
DEMO_USER,
type DemoEmail,
} from "@/components/demo/demo-mail-data"
import type {
ApiMailAccount,
ApiMessageFull,
ApiMessageSummary,
MessageSearchFilter,
PaginatedResponse,
Recipient,
} from "@/lib/api/types"
import { apiMessageToEmail } from "@/lib/demo/demo-mail-email-map"
import { emailMatchesFolder } from "@/lib/mail-folder-filter"
import type { Email } from "@/lib/email-data"
import { normalizeListPageSize } from "@/lib/mail-list-page-size"
export const DEMO_MAIL_ACCOUNT_ID = "demo-mail-account"
export const DEMO_MAIL_ACCOUNT: ApiMailAccount = {
id: DEMO_MAIL_ACCOUNT_ID,
name: DEMO_USER.name,
email: DEMO_USER.email,
provider: "demo",
imap_host: "demo.local",
smtp_host: "demo.local",
is_active: true,
created_at: "2026-06-01T08:00:00+02:00",
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
}
function demoTimeToIso(time: string): string {
const now = new Date()
const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())
if (time.includes(":")) {
const [h, m] = time.split(":").map(Number)
base.setHours(h, m ?? 0, 0, 0)
return base.toISOString()
}
if (time === "Hier") {
base.setDate(base.getDate() - 1)
base.setHours(14, 20, 0, 0)
return base.toISOString()
}
if (time === "Dim.") {
const day = base.getDay()
const diff = day === 0 ? 0 : day
base.setDate(base.getDate() - diff)
base.setHours(3, 0, 0, 0)
return base.toISOString()
}
if (time.startsWith("Lun")) {
const day = base.getDay()
const diff = day === 0 ? 6 : day - 1
base.setDate(base.getDate() - diff)
base.setHours(11, 0, 0, 0)
return base.toISOString()
}
const weekdayOffsets: Record<string, number> = {
"Mar.": 2,
"Mer.": 3,
"Jeu.": 4,
"Ven.": 5,
"Sam.": 6,
}
for (const [prefix, weekday] of Object.entries(weekdayOffsets)) {
if (time.startsWith(prefix)) {
const day = base.getDay()
const diff = day >= weekday ? day - weekday : day + (7 - weekday)
base.setDate(base.getDate() - diff)
base.setHours(10, 30, 0, 0)
return base.toISOString()
}
}
base.setHours(12, 0, 0, 0)
return base.toISOString()
}
function dedupeLabels(labels: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const label of labels) {
const key = label.toLowerCase()
if (seen.has(key)) continue
seen.add(key)
out.push(label)
}
return out
}
function demoTagLabels(email: DemoEmail): string[] {
const tags = [...(email.tags ?? [])]
if (
email.label &&
!tags.some((tag) => tag.toLowerCase() === email.label!.text.toLowerCase())
) {
tags.push(email.label.text)
}
return tags
}
function demoFolderToLabels(email: DemoEmail): string[] {
const tags = demoTagLabels(email)
if (email.folder === "trash") return ["trash"]
if (email.folder === "sent") return ["sent"]
if (email.folder === "drafts") return ["drafts"]
if (email.folder === "archive") return dedupeLabels(tags)
return dedupeLabels(["inbox", ...tags])
}
function demoBodyToHtml(body: string[]): string {
return body
.map(
(paragraph) =>
`<p style="margin:0 0 1em;line-height:1.6;">${escapeHtml(paragraph).replace(/\n/g, "<br>")}</p>`
)
.join("")
}
export function demoEmailToApiMessage(email: DemoEmail): ApiMessageFull {
const flags: string[] = []
if (!email.unread) flags.push("\\Seen")
if (email.starred) flags.push("\\Flagged")
if (email.important) flags.push("important")
return {
id: email.id,
message_id: email.id,
thread_id: email.id,
account_id: DEMO_MAIL_ACCOUNT_ID,
subject: email.subject,
from: [{ name: email.fromName, address: email.fromEmail }],
to: [{ name: DEMO_USER.name, address: DEMO_USER.email }],
date: demoTimeToIso(email.time),
snippet: email.preview,
flags,
labels: demoFolderToLabels(email),
has_attachments: email.hasAttachment ?? false,
body_html: demoBodyToHtml(email.body),
body_text: email.body.join("\n\n"),
}
}
export function createInitialDemoMessages(): ApiMessageFull[] {
return DEMO_EMAILS.map(demoEmailToApiMessage)
}
function toSummary(message: ApiMessageFull): ApiMessageSummary {
const { body_html: _bodyHtml, body_text: _bodyText, cc: _cc, reply_to: _replyTo, auth_info: _auth, in_reply_to: _inReply, references: _refs, ...summary } = message
return summary
}
function messageToEmail(message: ApiMessageSummary | ApiMessageFull): Email {
return apiMessageToEmail(message)
}
function matchesSearch(message: ApiMessageSummary, filter: MessageSearchFilter): boolean {
const email = messageToEmail(message)
const q = filter.q?.trim().toLowerCase()
if (q) {
const haystack = [
email.sender,
email.senderEmail ?? "",
email.subject,
email.preview,
message.snippet,
]
.join(" ")
.toLowerCase()
if (!haystack.includes(q)) return false
}
if (filter.from) {
const from = filter.from.toLowerCase()
const sender = message.from[0]?.address?.toLowerCase() ?? ""
const name = message.from[0]?.name?.toLowerCase() ?? ""
if (!sender.includes(from) && !name.includes(from)) return false
}
if (filter.label) {
const needle = filter.label.toLowerCase()
if (!message.labels.some((l) => l.toLowerCase() === needle)) return false
}
return true
}
export function listDemoMessages(
messages: ApiMessageFull[],
folder: string,
page: number,
pageSize: number
): PaginatedResponse<ApiMessageSummary> {
const safePageSize = normalizeListPageSize(pageSize)
const ctx = { starredEmailIds: [], importantEmailIds: [] }
const filtered = messages
.map(toSummary)
.filter((message) =>
emailMatchesFolder(messageToEmail(message), folder, ctx)
)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
const start = (page - 1) * safePageSize
const slice = filtered.slice(start, start + safePageSize)
return {
data: slice,
pagination: {
page,
page_size: safePageSize,
total: filtered.length,
},
}
}
export function searchDemoMessages(
messages: ApiMessageFull[],
filter: MessageSearchFilter | null,
page = 1,
pageSize = 50
): PaginatedResponse<ApiMessageSummary> {
const safePageSize = normalizeListPageSize(pageSize)
const filtered = messages
.map(toSummary)
.filter((message) => (filter ? matchesSearch(message, filter) : true))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
const start = (page - 1) * safePageSize
return {
data: filtered.slice(start, start + safePageSize),
pagination: {
page,
page_size: safePageSize,
total: filtered.length,
},
}
}
export function demoSentMessageFromPayload(payload: {
account_id: string
to: Recipient[]
subject: string
body_html: string
}): ApiMessageFull {
const id = `sent-${Date.now()}`
const text = payload.body_html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
return {
id,
message_id: id,
thread_id: id,
account_id: payload.account_id,
subject: payload.subject || "(Sans objet)",
from: [{ name: DEMO_USER.name, address: DEMO_USER.email }],
to: payload.to,
date: new Date().toISOString(),
snippet: text.slice(0, 160) || "(message vide)",
flags: ["\\Seen"],
labels: ["sent"],
has_attachments: false,
body_html: payload.body_html,
body_text: text,
}
}
export function moveDemoMessageToTrash(message: ApiMessageFull): ApiMessageFull {
const labels = message.labels.filter((l) => l.toLowerCase() !== "inbox")
if (!labels.some((l) => l.toLowerCase() === "trash")) {
labels.push("trash")
}
return { ...message, labels }
}