{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "email-viewer",
  "title": "Email Viewer",
  "description": "Render recursive MIME messages with headers, CID inline resources, nested messages, and File Viewer leaves.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/utils",
    "@retab/file-size-format",
    "@retab/file-thumbnail",
    "@retab/file-viewer",
    "@retab/viewer-controls"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/email-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { FileText, Layers3, Mail, Paperclip } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { useEmailInlineResourceUrls } from \"./email-viewer-inline-resources\";\nimport {\n  buildMimeTree,\n  deriveEmailInlineResourceScope,\n  deriveEmailViewerModel,\n  findMimeNodeByPath,\n  getDefaultMimeSelectionPath,\n} from \"./email-viewer-model\";\nimport type {\n  EmailAddress,\n  EmailContentModel,\n  EmailHeaderModel,\n  EmailSidebarItem,\n  EmailSidebarModel,\n  EmailViewerMessage,\n  EmailViewerModel,\n  EmailViewerProps,\n  EmailViewerProviderProps,\n  MimePartNode,\n  MimePartPath,\n} from \"./email-viewer-types\";\nimport { FileThumbnail } from \"./file-thumbnail\";\nimport { FileViewerPreview } from \"./file-viewer\";\nimport {\n  ViewerBody,\n  ViewerHeader,\n  ViewerRoot,\n  ViewerSidebar,\n  ViewerSidebarTrigger,\n  ViewerSurface,\n} from \"./viewer\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport type {\n  EmailAddress,\n  EmailAttachmentSidebarItem,\n  EmailBodySelectionPolicy,\n  EmailBodySidebarItem,\n  EmailContentEmpty,\n  EmailContentEmptyReason,\n  EmailContentFile,\n  EmailContentModel,\n  EmailContentNestedMessage,\n  EmailFilePayload,\n  EmailHeaderModel,\n  EmailInlineResource,\n  EmailInlineResourceKey,\n  EmailInlineResourceScope,\n  EmailSidebarItemBase,\n  EmailSidebarItem,\n  EmailSidebarModel,\n  EmailSidebarSection,\n  EmailSidebarThumbnailModel,\n  EmailViewerMessage,\n  EmailViewerModel,\n  EmailViewerProviderProps,\n  EmailViewerProps,\n  MimeHeader,\n  MimeMessage,\n  MimeMessageScope,\n  MimePart,\n  MimePartDisposition,\n  MimePartFacts,\n  MimePartKind,\n  MimePartNode,\n  MimePartPath,\n  MimePreviewPolicy,\n} from \"./email-viewer-types\";\n\nexport {\n  EmailResourceContent,\n  type EmailResourceContentProps,\n} from \"./email-viewer-content\";\n\nexport { parseEmlMessage, type ParseEmlOptions } from \"./email-viewer-eml\";\n\nexport {\n  buildMimeTree,\n  categoryForMimeNode,\n  createMimeMessageScope,\n  DEFAULT_EMAIL_BODY_SELECTION_POLICY,\n  deriveEmailContentModel,\n  deriveEmailHeaderModel,\n  deriveEmailInlineResourceScope,\n  deriveEmailSidebarModel,\n  deriveEmailViewerModel,\n  findMimeNodeByPath,\n  getDefaultMimeSelectionPath,\n  getInlineResourceScope,\n  inlineResourceKeyToString,\n  isAttachmentNode,\n  isInlineResourceNode,\n  isMessageNode,\n  isMultipartNode,\n  isRenderableNode,\n  normalizeContentId,\n  normalizeContentLocation,\n  pathsEqual,\n  replaceCidUrls,\n  replaceInlineResourceUrls,\n} from \"./email-viewer-model\";\n\nconst DEFAULT_MAX_NESTED_MESSAGE_DEPTH = 8;\n\ntype EmailViewerContextValue = {\n  model: EmailViewerModel;\n  selectPart: (node: MimePartNode) => void;\n};\n\ntype EmailViewerProviderInternalProps = EmailViewerProviderProps & {\n  nestedMessageDepth?: number;\n};\n\ntype EmailViewerInternalProps = EmailViewerProps & {\n  nestedMessageDepth?: number;\n};\n\ntype EmailViewerLayoutProps = Pick<\n  EmailViewerInternalProps,\n  \"className\" | \"mode\"\n>;\n\nconst EmailViewerContext = React.createContext<EmailViewerContextValue | null>(\n  null,\n);\n\nfunction useEmailViewerContext() {\n  const context = React.useContext(EmailViewerContext);\n  if (!context) {\n    throw new Error(\"EmailViewerProvider context is missing.\");\n  }\n  return context;\n}\n\nfunction useEmailViewerHeaderState(): EmailHeaderModel {\n  return useEmailViewerContext().model.header;\n}\n\nfunction useEmailViewerPartsSidebarState(): {\n  sidebar: EmailSidebarModel;\n  selectPart: (node: MimePartNode) => void;\n} {\n  const { model, selectPart } = useEmailViewerContext();\n\n  return {\n    sidebar: model.sidebar,\n    selectPart,\n  };\n}\n\nfunction useEmailViewerContentState(): EmailContentModel {\n  return useEmailViewerContext().model.content;\n}\n\nexport function EmailViewerProvider(props: EmailViewerProviderProps) {\n  return <EmailViewerProviderInternal {...props} nestedMessageDepth={0} />;\n}\n\nfunction EmailViewerProviderInternal({\n  message,\n  selectedPath,\n  defaultSelectedPath,\n  onSelectedPathChange,\n  maxNestedMessageDepth = DEFAULT_MAX_NESTED_MESSAGE_DEPTH,\n  nestedMessageDepth = 0,\n  children,\n}: EmailViewerProviderInternalProps) {\n  const rootNode = React.useMemo(\n    () => buildMimeTree(message.root),\n    [message.root],\n  );\n  const defaultPath = React.useMemo(\n    () =>\n      defaultSelectedPath && findMimeNodeByPath(rootNode, defaultSelectedPath)\n        ? defaultSelectedPath\n        : getDefaultMimeSelectionPath(rootNode),\n    [defaultSelectedPath, rootNode],\n  );\n  const [internalSelectedPath, setInternalSelectedPath] =\n    React.useState<MimePartPath>(defaultPath);\n  const controlled = selectedPath !== undefined;\n  const activePath = controlled\n    ? (selectedPath ?? defaultPath)\n    : internalSelectedPath;\n  const selectedNode =\n    findMimeNodeByPath(rootNode, activePath) ??\n    findMimeNodeByPath(rootNode, defaultPath) ??\n    rootNode;\n  const inlineResourceScope = React.useMemo(\n    () => deriveEmailInlineResourceScope(rootNode, selectedNode),\n    [rootNode, selectedNode],\n  );\n  const inlineResourceUrls = useEmailInlineResourceUrls(inlineResourceScope);\n\n  useKeyedMountEffect(\n    joinEffectKey([controlled, defaultPath, internalSelectedPath, rootNode]),\n    () => {\n      if (controlled) return;\n      if (findMimeNodeByPath(rootNode, internalSelectedPath)) return;\n      setInternalSelectedPath(defaultPath);\n    },\n  );\n\n  const selectPart = React.useCallback(\n    (node: MimePartNode) => {\n      if (!controlled) setInternalSelectedPath(node.path);\n      onSelectedPathChange?.(node.path, node);\n    },\n    [controlled, onSelectedPathChange],\n  );\n  const model = React.useMemo(\n    () =>\n      deriveEmailViewerModel({\n        inlineResourceUrls,\n        maxNestedMessageDepth,\n        message,\n        nestedMessageDepth,\n        rootNode,\n        selectedNode,\n      }),\n    [\n      inlineResourceUrls,\n      maxNestedMessageDepth,\n      message,\n      nestedMessageDepth,\n      rootNode,\n      selectedNode,\n    ],\n  );\n  const value = React.useMemo<EmailViewerContextValue>(\n    () => ({ model, selectPart }),\n    [model, selectPart],\n  );\n\n  return (\n    <EmailViewerContext.Provider value={value}>\n      {children}\n    </EmailViewerContext.Provider>\n  );\n}\n\nexport function EmailViewer(props: EmailViewerProps) {\n  return <EmailViewerInternal {...props} nestedMessageDepth={0} />;\n}\n\nfunction EmailViewerInternal({\n  message,\n  selectedPath,\n  defaultSelectedPath,\n  onSelectedPathChange,\n  maxNestedMessageDepth,\n  mode,\n  nestedMessageDepth = 0,\n  className,\n}: EmailViewerInternalProps) {\n  if (nestedMessageDepth === 0) {\n    return (\n      <EmailViewerProvider\n        message={message}\n        selectedPath={selectedPath}\n        defaultSelectedPath={defaultSelectedPath}\n        onSelectedPathChange={onSelectedPathChange}\n        maxNestedMessageDepth={maxNestedMessageDepth}\n      >\n        <EmailViewerLayout className={className} mode={mode} />\n      </EmailViewerProvider>\n    );\n  }\n\n  return (\n    <EmailViewerProviderInternal\n      message={message}\n      selectedPath={selectedPath}\n      defaultSelectedPath={defaultSelectedPath}\n      onSelectedPathChange={onSelectedPathChange}\n      maxNestedMessageDepth={maxNestedMessageDepth}\n      nestedMessageDepth={nestedMessageDepth}\n    >\n      <EmailViewerLayout className={className} mode={mode} />\n    </EmailViewerProviderInternal>\n  );\n}\n\nfunction EmailViewerLayout({ className, mode }: EmailViewerLayoutProps) {\n  return (\n    <div data-slot=\"email-viewer\" className={cn(\"min-h-0\", className)}>\n      <ViewerRoot\n        defaultOpen\n        mode={mode}\n        sidebarSide=\"right\"\n        className=\"h-full\"\n      >\n        <EmailViewerHeader />\n        <ViewerBody className=\"flex-col md:flex-row\">\n          <ViewerSurface className=\"min-h-[26rem] md:min-h-0\">\n            <EmailViewerContent />\n          </ViewerSurface>\n          <ViewerSidebar\n            aria-label=\"Email parts\"\n            width=\"19rem\"\n            className=\"border-t md:border-t-0 md:border-l\"\n          >\n            <EmailViewerPartsSidebar />\n          </ViewerSidebar>\n        </ViewerBody>\n      </ViewerRoot>\n    </div>\n  );\n}\n\nexport function EmailViewerHeader({\n  trailing = <ViewerSidebarTrigger className=\"-mr-1\" />,\n}: {\n  trailing?: React.ReactNode;\n}) {\n  return (\n    <MimeMessageHeader\n      header={useEmailViewerHeaderState()}\n      trailing={trailing}\n    />\n  );\n}\n\nexport function EmailViewerPartsSidebar() {\n  const { sidebar, selectPart } = useEmailViewerPartsSidebarState();\n\n  return <MimePartSidebar sidebar={sidebar} onSelectPart={selectPart} />;\n}\n\nexport function EmailViewerContent() {\n  const content = useEmailViewerContentState();\n\n  if (content.kind === \"nested-message\") {\n    return (\n      <EmailViewerInternal\n        className=\"h-full\"\n        message={content.message}\n        maxNestedMessageDepth={content.maxNestedMessageDepth}\n        nestedMessageDepth={content.nestedMessageDepth}\n      />\n    );\n  }\n\n  if (content.kind === \"empty\") {\n    return (\n      <div className=\"text-muted-foreground flex size-full items-center justify-center px-6 text-center text-sm\">\n        {content.message}\n      </div>\n    );\n  }\n\n  return (\n    <FileViewerPreview\n      key={content.node.path.join(\"/\")}\n      source={content.file.source}\n      category={content.file.category}\n      className=\"size-full min-h-0\"\n    />\n  );\n}\n\nfunction MimeMessageHeader({\n  header,\n  trailing,\n}: {\n  header: EmailHeaderModel;\n  trailing?: React.ReactNode;\n}) {\n  const from = formatEmailAddresses(header.from);\n  const to = formatEmailAddresses(header.to);\n\n  return (\n    <ViewerHeader className=\"px-3 py-2\">\n      <div\n        data-slot=\"email-message-header\"\n        className=\"flex min-h-0 flex-col gap-1\"\n      >\n        <div className=\"flex min-w-0 items-center gap-2\">\n          <Mail className=\"text-muted-foreground size-4 flex-shrink-0\" />\n          <h2 className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n            {header.subject}\n          </h2>\n          {trailing}\n        </div>\n        <div className=\"text-muted-foreground flex min-w-0 flex-wrap gap-x-3 gap-y-1 pl-6 text-xs\">\n          {from ? <span className=\"min-w-0 truncate\">From {from}</span> : null}\n          {to ? <span className=\"min-w-0 truncate\">To {to}</span> : null}\n          {header.sentAt ? (\n            <span className=\"tabular-nums\">{header.sentAt}</span>\n          ) : null}\n        </div>\n      </div>\n    </ViewerHeader>\n  );\n}\n\nfunction MimePartSidebar({\n  sidebar,\n  onSelectPart,\n  className,\n}: {\n  sidebar: EmailSidebarModel;\n  onSelectPart: (node: MimePartNode) => void;\n  className?: string;\n}) {\n  return (\n    <div\n      data-slot=\"mime-part-sidebar\"\n      className={cn(\n        \"bg-background text-foreground flex h-full min-h-0 flex-col\",\n        className,\n      )}\n    >\n      <div className=\"flex-shrink-0 border-b px-3 py-2\">\n        <div className=\"flex h-6 items-center gap-2 text-xs font-medium\">\n          <Paperclip className=\"text-muted-foreground size-3.5\" />\n          <span>\n            {sidebar.attachmentCount} attachment\n            {sidebar.attachmentCount === 1 ? \"\" : \"s\"}\n          </span>\n        </div>\n      </div>\n      <div className=\"min-h-0 flex-1 space-y-2 overflow-auto p-2\">\n        {sidebar.sections.map((section) => (\n          <MimePartSidebarSection key={section.id} title={section.title}>\n            {section.items.length === 0 ? (\n              section.emptyLabel ? (\n                <p className=\"text-muted-foreground px-2 py-3 text-xs\">\n                  {section.emptyLabel}\n                </p>\n              ) : null\n            ) : (\n              <ul className=\"flex flex-col gap-1\">\n                {section.items.map((item) => (\n                  <MimePartSidebarItem\n                    key={item.id}\n                    item={item}\n                    onSelectPart={onSelectPart}\n                  />\n                ))}\n              </ul>\n            )}\n          </MimePartSidebarSection>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction formatEmailAddresses(addresses: readonly EmailAddress[]) {\n  return addresses.map((address) => address.display).join(\", \") || null;\n}\n\nfunction MimePartSidebarSection({\n  title,\n  children,\n}: {\n  title: string;\n  children: React.ReactNode;\n}) {\n  const titleId = React.useId();\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      data-slot=\"mime-part-sidebar-section\"\n      className=\"min-w-0\"\n    >\n      <h3\n        id={titleId}\n        className=\"text-muted-foreground flex h-8 shrink-0 items-center px-2 text-xs font-medium\"\n      >\n        {title}\n      </h3>\n      {children}\n    </section>\n  );\n}\n\nfunction MimePartSidebarItem({\n  item,\n  onSelectPart,\n}: {\n  item: EmailSidebarItem;\n  onSelectPart: (node: MimePartNode) => void;\n}) {\n  return (\n    <li data-slot=\"mime-part-sidebar-item\">\n      <button\n        type=\"button\"\n        aria-current={item.isSelected ? \"page\" : undefined}\n        aria-label={`${item.title} ${item.description}`}\n        data-selected={item.isSelected ? \"true\" : \"false\"}\n        className={cn(\n          \"hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring active:bg-accent flex h-auto w-full items-center gap-3 overflow-hidden rounded-lg border p-2 text-left text-sm outline-hidden transition-colors focus-visible:ring-2\",\n          item.isSelected\n            ? \"border-border bg-accent text-accent-foreground\"\n            : \"border-transparent\",\n        )}\n        onClick={() => onSelectPart(item.node)}\n      >\n        <SidebarItemThumbnail item={item} />\n        <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n          <span className=\"truncate text-sm font-medium\">{item.title}</span>\n          <span\n            className={cn(\n              \"truncate text-xs\",\n              item.isSelected\n                ? \"text-accent-foreground/80\"\n                : \"text-muted-foreground\",\n            )}\n          >\n            {item.description}\n          </span>\n        </span>\n      </button>\n    </li>\n  );\n}\n\nfunction SidebarItemThumbnail({ item }: { item: EmailSidebarItem }) {\n  if (item.thumbnail.kind === \"file\") {\n    return (\n      <FileThumbnail\n        source={item.thumbnail.source}\n        presentation=\"decorative\"\n        thumbnailShape=\"square\"\n        thumbnailSize=\"md\"\n        className=\"flex-shrink-0\"\n      />\n    );\n  }\n\n  return (\n    <span\n      className={cn(\n        \"bg-muted/60 flex size-12 flex-shrink-0 items-center justify-center rounded-md\",\n        item.isSelected ? \"text-accent-foreground\" : \"text-muted-foreground\",\n      )}\n    >\n      <PartIcon icon={item.thumbnail.icon} className=\"size-4\" />\n    </span>\n  );\n}\n\nfunction PartIcon({\n  icon,\n  className,\n}: {\n  icon: \"file\" | \"layers\" | \"mail\" | \"paperclip\";\n  className?: string;\n}) {\n  if (icon === \"layers\") return <Layers3 className={className} aria-hidden />;\n  if (icon === \"mail\") return <Mail className={className} aria-hidden />;\n  if (icon === \"paperclip\") {\n    return <Paperclip className={className} aria-hidden />;\n  }\n  return <FileText className={className} aria-hidden />;\n}\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/viewer.tsx",
      "content": "\"use client\";\n\nexport { ViewerBody } from \"./viewer-body\";\nexport type { ViewerSidebarTriggerProps } from \"./viewer-chrome\";\nexport {\n  ViewerFrame,\n  ViewerHeader,\n  ViewerSidebarTrigger,\n} from \"./viewer-chrome\";\nexport {\n  ViewerRoot,\n  useOptionalViewerSidebar,\n  useViewerSidebar,\n} from \"./viewer-root\";\nexport { ViewerSidebar } from \"./viewer-sidebar\";\nexport { ViewerSurface, ViewerViewport } from \"./viewer-surface\";\nexport type {\n  ViewerBodyProps,\n  ViewerDataAttributes,\n  ViewerFrameProps,\n  ViewerHeaderProps,\n  ViewerRootProps,\n  ViewerSidebarCollapsible,\n  ViewerSidebarStateValue,\n  ViewerSidebarMode,\n  ViewerSidebarProps,\n  ViewerSidebarRequestedMode,\n  ViewerSidebarSide,\n  ViewerSidebarSlotNames,\n  ViewerSidebarState,\n  ViewerStateAttributeNamespace,\n  ViewerStateAttributeSlot,\n  ViewerStateAttributeValues,\n  ViewerSurfaceProps,\n  ViewerViewportProps,\n} from \"./viewer-types\";\n",
      "type": "registry:ui",
      "target": "@ui/viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/email-viewer-model.ts",
      "content": "import {\n  detectCategory,\n  type FileCategory,\n  type ViewerSource,\n} from \"@/lib/viewer-source\";\n\nimport type {\n  EmailAddress,\n  EmailBodySelectionPolicy,\n  EmailContentEmptyReason,\n  EmailContentModel,\n  EmailHeaderModel,\n  EmailInlineResource,\n  EmailInlineResourceKey,\n  EmailInlineResourceScope,\n  EmailSidebarItem,\n  EmailSidebarModel,\n  EmailSidebarThumbnailModel,\n  EmailViewerMessage,\n  EmailViewerModel,\n  MimeMessage,\n  MimeMessageScope,\n  MimePart,\n  MimePartFacts,\n  MimePartKind,\n  MimePartNode,\n  MimePartPath,\n  MimePreviewPolicy,\n} from \"./email-viewer-types\";\nimport { formatFileSize } from \"./file-size-format\";\n\nconst DEFAULT_EMPTY_CONTENT_MESSAGE =\n  \"This MIME part does not have a previewable body.\";\nconst DEFAULT_MAX_NESTED_MESSAGE_DEPTH = 8;\nconst EMPTY_INLINE_RESOURCE_URLS = new Map<string, string>();\nexport const MISSING_EMAIL_INLINE_RESOURCE_URL =\n  \"data:image/gif;base64,R0lGODlhAQABAAAAACw=\";\n\nexport const DEFAULT_EMAIL_BODY_SELECTION_POLICY = {\n  preferredMimeTypes: [\"text/html\", \"text/plain\", \"text/markdown\"],\n  includeInlineBodyParts: true,\n  includeAttachments: false,\n} satisfies EmailBodySelectionPolicy;\n\nexport function buildMimeTree(root: MimePart): MimePartNode {\n  return buildMimeNode({\n    part: root,\n    parentPath: null,\n    depth: 0,\n    pathPart: normalizePathPart(root.id, 0),\n  });\n}\n\nexport function findMimeNodeByPath(\n  root: MimePartNode,\n  path: MimePartPath,\n): MimePartNode | null {\n  if (pathsEqual(root.path, path)) return root;\n  for (const child of root.children) {\n    const match = findMimeNodeByPath(child, path);\n    if (match) return match;\n  }\n  return null;\n}\n\nexport function getDefaultMimeSelectionPath(root: MimePartNode): MimePartPath {\n  return (selectMimeScopeBodyNode(root) ?? root).path;\n}\n\nexport function deriveEmailViewerModel({\n  inlineResourceUrls,\n  maxNestedMessageDepth = DEFAULT_MAX_NESTED_MESSAGE_DEPTH,\n  message,\n  nestedMessageDepth = 0,\n  rootNode,\n  selectedNode,\n}: {\n  inlineResourceUrls: ReadonlyMap<string, string>;\n  maxNestedMessageDepth?: number;\n  message: EmailViewerMessage;\n  nestedMessageDepth?: number;\n  rootNode: MimePartNode;\n  selectedNode: MimePartNode;\n}): EmailViewerModel {\n  const scope = createMimeMessageScope(message, rootNode);\n  const selectedPath = selectedNode.path;\n\n  return {\n    message,\n    rootNode,\n    scope,\n    selectedPath,\n    selectedNode,\n    header: deriveEmailHeaderModel(message),\n    sidebar: deriveEmailSidebarModel({\n      inlineResourceUrls,\n      scope,\n      selectedPath,\n    }),\n    content: deriveEmailContentModel({\n      inlineResourceUrls,\n      maxNestedMessageDepth,\n      message,\n      nestedMessageDepth,\n      selectedNode,\n    }),\n  };\n}\n\nexport function createMimeMessageScope(\n  message: EmailViewerMessage,\n  root: MimePartNode,\n): MimeMessageScope {\n  return {\n    message,\n    root,\n    path: root.path,\n    descendants: collectCurrentMessageNodes(root),\n  };\n}\n\nexport function deriveEmailHeaderModel(\n  message: EmailViewerMessage,\n): EmailHeaderModel {\n  return {\n    subject: message.subject?.trim() || \"(no subject)\",\n    from: normalizeAddressList(message.from),\n    to: normalizeAddressList(message.to),\n    cc: normalizeAddressList(message.cc),\n    bcc: normalizeAddressList(message.bcc),\n    sentAt: formatSentAt(message.sentAt),\n  };\n}\n\nexport function deriveEmailSidebarModel({\n  inlineResourceUrls = EMPTY_INLINE_RESOURCE_URLS,\n  scope,\n  selectedPath,\n}: {\n  inlineResourceUrls?: ReadonlyMap<string, string>;\n  scope: MimeMessageScope;\n  selectedPath: MimePartPath;\n}): EmailSidebarModel {\n  const bodyNode = selectMimeScopeBodyNode(scope.root);\n  const bodyItems: readonly EmailSidebarItem[] = [\n    createSidebarItem({\n      inlineResourceUrls,\n      kind: \"body\",\n      node: bodyNode ?? scope.root,\n      selectedPath,\n    }),\n  ];\n  const attachmentItems = scope.descendants\n    .filter((node) => isEmailAttachmentSidebarNode(node, bodyNode))\n    .map((node) =>\n      createSidebarItem({\n        inlineResourceUrls,\n        kind: \"attachment\",\n        node,\n        selectedPath,\n      }),\n    );\n  const sections = [\n    {\n      id: \"body\" as const,\n      title: \"Body\",\n      items: bodyItems,\n    },\n    {\n      id: \"attachments\" as const,\n      title: \"Attachments\",\n      items: attachmentItems,\n      emptyLabel: \"No attachments.\",\n    },\n  ];\n\n  return {\n    bodyCount: bodyItems.length,\n    attachmentCount: attachmentItems.length,\n    sections,\n  };\n}\n\nexport function deriveEmailContentModel({\n  inlineResourceUrls,\n  maxNestedMessageDepth = DEFAULT_MAX_NESTED_MESSAGE_DEPTH,\n  message,\n  nestedMessageDepth = 0,\n  selectedNode,\n}: {\n  inlineResourceUrls: ReadonlyMap<string, string>;\n  maxNestedMessageDepth?: number;\n  message: EmailViewerMessage;\n  nestedMessageDepth?: number;\n  selectedNode: MimePartNode;\n}): EmailContentModel {\n  if (isMessageNode(selectedNode) && selectedNode.children.length > 0) {\n    if (nestedMessageDepth >= maxNestedMessageDepth) {\n      return createEmptyContent({\n        message: \"This nested message is too deeply nested to preview.\",\n        node: selectedNode,\n        reason: \"nested-depth-exceeded\",\n      });\n    }\n\n    return {\n      kind: \"nested-message\",\n      maxNestedMessageDepth,\n      node: selectedNode,\n      message: deriveNestedEmailMessage(message, selectedNode),\n      nestedMessageDepth: nestedMessageDepth + 1,\n    };\n  }\n\n  if (selectedNode.facts.preview.kind === \"security-envelope\") {\n    return createEmptyContent({\n      message: `${selectedNode.facts.preview.label} cannot be previewed in this viewer.`,\n      node: selectedNode,\n      reason: \"security-envelope\",\n    });\n  }\n\n  const fileNode = selectDefaultPreviewNode(selectedNode, {\n    stopAtNestedMessages: false,\n  });\n\n  if (!fileNode) {\n    return createEmptyContent({\n      message: DEFAULT_EMPTY_CONTENT_MESSAGE,\n      node: selectedNode,\n      reason:\n        selectedNode.facts.preview.kind === \"unsupported\"\n          ? selectedNode.facts.preview.reason\n          : \"no-previewable-body\",\n    });\n  }\n\n  if (fileNode.facts.preview.kind === \"security-envelope\") {\n    return createEmptyContent({\n      message: `${fileNode.facts.preview.label} cannot be previewed in this viewer.`,\n      node: fileNode,\n      reason: \"security-envelope\",\n    });\n  }\n\n  if (!fileNode.part.source) {\n    return createEmptyContent({\n      message: \"This MIME part is missing a preview source.\",\n      node: fileNode,\n      reason: \"missing-source\",\n    });\n  }\n\n  return {\n    kind: \"file\",\n    node: fileNode,\n    file: {\n      category: categoryForMimeNode(fileNode),\n      source: resolveEmailPreviewSource(fileNode, inlineResourceUrls),\n    },\n  };\n}\n\nexport function deriveEmailInlineResourceScope(\n  rootNode: MimePartNode,\n  selectedNode: MimePartNode,\n): EmailInlineResourceScope {\n  if (isMessageNode(selectedNode)) {\n    return {\n      root: selectedNode,\n      resources: [],\n    };\n  }\n\n  const displayNode = selectDefaultPreviewNode(selectedNode, {\n    stopAtNestedMessages: true,\n  });\n  const root = displayNode\n    ? getInlineResourceScope(rootNode, displayNode)\n    : selectedNode;\n\n  return {\n    root,\n    resources: collectInlineResourceParts(root),\n  };\n}\n\nexport function collectInlineResourceParts(\n  node: MimePartNode,\n): readonly EmailInlineResource[] {\n  const resources: EmailInlineResource[] = [];\n\n  walkMimeTree(node, (current) => {\n    if (!isInlineResourceNode(current) || !current.part.source) return;\n\n    const keys = inlineResourceKeysForNode(current);\n    if (keys.length > 0) resources.push({ node: current, keys });\n  });\n\n  return resources;\n}\n\nexport function getInlineResourceScope(\n  rootNode: MimePartNode,\n  node: MimePartNode,\n): MimePartNode {\n  let current: MimePartNode | null = node;\n  while (current) {\n    if (isRelatedMultipart(current.facts.mimeType)) return current;\n    current = getParentMimeNode(rootNode, current);\n  }\n  return node;\n}\n\nexport function normalizeContentId(contentId: string | null | undefined) {\n  const trimmed = contentId?.trim();\n  if (!trimmed) return null;\n  return trimmed.replace(/^<|>$/g, \"\").toLowerCase();\n}\n\nexport function normalizeContentLocation(\n  contentLocation: string | null | undefined,\n) {\n  const trimmed = contentLocation?.trim();\n  if (!trimmed) return null;\n  return normalizeRelativeReference(trimmed);\n}\n\nexport function inlineResourceKeyToString(key: EmailInlineResourceKey) {\n  return `${key.kind}:${key.value}`;\n}\n\nexport function replaceInlineResourceUrls(\n  html: string,\n  inlineUrls: ReadonlyMap<string, string>,\n) {\n  return replaceContentLocationUrls(\n    replaceCidUrls(html, inlineUrls),\n    inlineUrls,\n  );\n}\n\nexport function replaceCidUrls(\n  html: string,\n  inlineUrls: ReadonlyMap<string, string>,\n) {\n  return html.replace(\n    /\\bcid:(?:<([^>\"'\\s)]+)>|([^\"'\\s>)]+))/gi,\n    (match, bracketedContentId, plainContentId) => {\n      const rawContentId = bracketedContentId ?? plainContentId;\n      const cid = normalizeContentId(decodeCid(rawContentId));\n      if (!cid) return match;\n      return (\n        inlineUrls.get(\n          inlineResourceKeyToString({ kind: \"content-id\", value: cid }),\n        ) ?? MISSING_EMAIL_INLINE_RESOURCE_URL\n      );\n    },\n  );\n}\n\nexport function mimePartLabel(part: MimePart): string {\n  const fileName = part.fileName?.trim();\n  if (fileName) return fileName;\n  if (isMultipartMime(part.mimeType)) return multipartLabel(part.mimeType);\n  if (isMessageMime(part.mimeType)) return \"Message\";\n  if (normalizedMimeType(part.mimeType) === \"text/html\") return \"HTML body\";\n  if (normalizedMimeType(part.mimeType) === \"text/plain\") return \"Text body\";\n  if (isSecurityEnvelopeMime(part.mimeType))\n    return securityEnvelopeLabel(part.mimeType);\n  return part.mimeType || \"MIME part\";\n}\n\nexport function mimePartDescription(part: MimePart): string {\n  const pieces = [\n    part.mimeType || null,\n    part.disposition?.trim() || null,\n    part.contentId ? `cid:${normalizeContentId(part.contentId)}` : null,\n    contentLocationForPart(part)\n      ? `location:${contentLocationForPart(part)}`\n      : null,\n  ].filter(Boolean);\n  return pieces.join(\" · \");\n}\n\nexport function categoryForMimePart(part: MimePart): FileCategory | undefined {\n  const mime = normalizedMimeType(part.mimeType);\n  if (mime === \"text/calendar\" || mime === \"application/ics\") return \"text\";\n  if (mime === \"message/delivery-status\") return \"text\";\n  if (mime === \"message/disposition-notification\") return \"text\";\n\n  const fileName = part.fileName?.trim() || part.source?.fileName || mime;\n  const sourceMimeType = part.source?.mimeType || part.mimeType || undefined;\n  const category = detectCategory(fileName, sourceMimeType);\n\n  return category === \"unsupported\" ? undefined : category;\n}\n\nexport function categoryForMimeNode(\n  node: MimePartNode,\n): FileCategory | undefined {\n  const policy = node.facts.preview;\n  if (policy.kind === \"preview\" || policy.kind === \"attachment\") {\n    return policy.category;\n  }\n  return categoryForMimePart(node.part);\n}\n\nexport function messageIdentity(message: MimeMessage): string {\n  return message.id ?? message.root.id;\n}\n\nexport function pathsEqual(left: MimePartPath, right: MimePartPath) {\n  if (left.length !== right.length) return false;\n  return left.every((part, index) => part === right[index]);\n}\n\nexport function isMultipartNode(node: MimePartNode) {\n  return node.facts.kind === \"multipart\";\n}\n\nexport function isMessageNode(node: MimePartNode) {\n  return node.facts.kind === \"message\";\n}\n\nexport function isRenderableNode(node: MimePartNode) {\n  return node.facts.isRenderable;\n}\n\nexport function isAttachmentNode(node: MimePartNode) {\n  return node.facts.kind === \"attachment\";\n}\n\nexport function isInlineResourceNode(node: MimePartNode) {\n  return node.facts.kind === \"inline-resource\";\n}\n\nfunction buildMimeNode({\n  part,\n  parentPath,\n  depth,\n  pathPart,\n}: {\n  part: MimePart;\n  parentPath: MimePartPath | null;\n  depth: number;\n  pathPart: string;\n}): MimePartNode {\n  const path = [...(parentPath ?? []), pathPart];\n  const facts = deriveMimePartFacts(part);\n  const childIds = new Map<string, number>();\n\n  return {\n    depth,\n    facts,\n    parentPath,\n    part,\n    path,\n    children: (part.children ?? []).map((child) => {\n      const childPathPart = uniqueChildPathPart(child.id, childIds);\n      return buildMimeNode({\n        part: child,\n        parentPath: path,\n        depth: depth + 1,\n        pathPart: childPathPart,\n      });\n    }),\n  };\n}\n\nfunction deriveMimePartFacts(part: MimePart): MimePartFacts {\n  const mimeType = normalizedMimeType(part.mimeType);\n  const disposition = part.disposition?.toLowerCase().trim() ?? null;\n  const contentId = normalizeContentId(part.contentId);\n  const contentLocation = contentLocationForPart(part);\n  const isRenderable = Boolean(part.source);\n  const kind = deriveMimePartKind({\n    contentId,\n    contentLocation,\n    disposition,\n    isRenderable,\n    mimeType,\n    part,\n  });\n\n  return {\n    contentId,\n    contentLocation,\n    disposition,\n    isRenderable,\n    kind,\n    mimeType,\n    preview: deriveMimePreviewPolicy({ isRenderable, kind, mimeType, part }),\n  };\n}\n\nfunction deriveMimePartKind({\n  contentId,\n  contentLocation,\n  disposition,\n  isRenderable,\n  mimeType,\n  part,\n}: {\n  contentId: string | null;\n  contentLocation: string | null;\n  disposition: string | null;\n  isRenderable: boolean;\n  mimeType: string;\n  part: MimePart;\n}): MimePartKind {\n  if (isMultipartMime(mimeType)) return \"multipart\";\n  if (isMessageMime(mimeType)) return \"message\";\n  if (\n    isRenderable &&\n    disposition !== \"attachment\" &&\n    !isBodyMime(mimeType) &&\n    Boolean(contentId || contentLocation)\n  ) {\n    return \"inline-resource\";\n  }\n  if (disposition === \"attachment\") return \"attachment\";\n  if (isRenderable && isBodyMime(mimeType)) return \"body\";\n  if (isRenderable && part.fileName && disposition === \"inline\") {\n    return \"attachment\";\n  }\n  if (isRenderable && !isSecurityEnvelopeMime(mimeType)) return \"attachment\";\n  return \"unsupported\";\n}\n\nfunction deriveMimePreviewPolicy({\n  isRenderable,\n  kind,\n  mimeType,\n  part,\n}: {\n  isRenderable: boolean;\n  kind: MimePartKind;\n  mimeType: string;\n  part: MimePart;\n}): MimePreviewPolicy {\n  if (isSecurityEnvelopeMime(mimeType)) {\n    return {\n      kind: \"security-envelope\",\n      label: securityEnvelopeLabel(mimeType),\n    };\n  }\n  if (kind === \"message\") return { kind: \"nested-message\" };\n  if (kind === \"attachment\") {\n    return { kind: \"attachment\", category: categoryForMimePart(part) };\n  }\n  if (isRenderable && kind === \"body\") {\n    return { kind: \"preview\", category: categoryForMimePart(part) };\n  }\n  if (isDeliveryStatusMime(mimeType)) {\n    return { kind: \"unsupported\", reason: \"unsupported-part\" };\n  }\n  return {\n    kind: \"unsupported\",\n    reason: isRenderable ? \"unsupported-part\" : \"missing-source\",\n  };\n}\n\nfunction uniqueChildPathPart(id: string, siblingCounts: Map<string, number>) {\n  const base = normalizePathPart(id, siblingCounts.size);\n  const count = siblingCounts.get(base) ?? 0;\n  siblingCounts.set(base, count + 1);\n  return count === 0 ? base : `${base}~${count + 1}`;\n}\n\nfunction normalizePathPart(id: string, fallbackIndex: number) {\n  const trimmed = id.trim();\n  return trimmed || `part-${fallbackIndex + 1}`;\n}\n\nfunction selectMimeScopeBodyNode(\n  root: MimePartNode,\n  policy: EmailBodySelectionPolicy = DEFAULT_EMAIL_BODY_SELECTION_POLICY,\n): MimePartNode | null {\n  const candidates: MimePartNode[] = [];\n\n  walkCurrentMessageNodes(root, (node) => {\n    if (!isRenderableNode(node)) return;\n    if (!policy.includeInlineBodyParts && isInlineResourceNode(node)) return;\n    if (!policy.includeAttachments && isAttachmentNode(node)) return;\n    if (isInlineResourceNode(node) || isMessageNode(node)) return;\n    candidates.push(node);\n  });\n\n  for (const mimeType of policy.preferredMimeTypes) {\n    const match = candidates.find((node) => node.facts.mimeType === mimeType);\n    if (match) return match;\n  }\n\n  return (\n    candidates[0] ??\n    selectDefaultPreviewNode(root, { stopAtNestedMessages: true })\n  );\n}\n\nfunction selectDefaultPreviewNode(\n  node: MimePartNode,\n  {\n    stopAtNestedMessages,\n  }: {\n    stopAtNestedMessages: boolean;\n  },\n): MimePartNode | null {\n  if (isMessageNode(node) && stopAtNestedMessages) return null;\n  if (isPreviewLeafNode(node)) return node;\n  if (!node.children.length) return null;\n\n  if (\n    node.facts.mimeType === \"multipart/alternative\" ||\n    node.facts.mimeType === \"multipart/related\"\n  ) {\n    return (\n      findFirstRenderableOfMime(node, \"text/html\", { stopAtNestedMessages }) ??\n      findFirstRenderableOfMime(node, \"text/plain\", {\n        stopAtNestedMessages,\n      }) ??\n      findFirstRenderableChild(node, { stopAtNestedMessages })\n    );\n  }\n\n  return findFirstRenderableChild(node, { stopAtNestedMessages });\n}\n\nfunction findFirstRenderableOfMime(\n  node: MimePartNode,\n  mimeType: string,\n  options: { stopAtNestedMessages: boolean },\n): MimePartNode | null {\n  for (const child of node.children) {\n    if (isMessageNode(child) && options.stopAtNestedMessages) continue;\n    if (\n      child.facts.mimeType === mimeType &&\n      isRenderableNode(child) &&\n      !isInlineResourceNode(child)\n    ) {\n      return child;\n    }\n    const match = findFirstRenderableOfMime(child, mimeType, options);\n    if (match) return match;\n  }\n  return null;\n}\n\nfunction findFirstRenderableChild(\n  node: MimePartNode,\n  options: { stopAtNestedMessages: boolean },\n): MimePartNode | null {\n  for (const child of node.children) {\n    const match = selectDefaultPreviewNode(child, options);\n    if (match) return match;\n  }\n  return null;\n}\n\nfunction isPreviewLeafNode(node: MimePartNode) {\n  if (!isRenderableNode(node) || isInlineResourceNode(node)) return false;\n  return (\n    node.facts.preview.kind === \"preview\" ||\n    node.facts.preview.kind === \"attachment\"\n  );\n}\n\nfunction collectCurrentMessageNodes(root: MimePartNode) {\n  const nodes: MimePartNode[] = [];\n  walkCurrentMessageNodes(root, (node) => nodes.push(node));\n  return nodes;\n}\n\nfunction walkCurrentMessageNodes(\n  root: MimePartNode,\n  visit: (node: MimePartNode) => void,\n) {\n  function walk(node: MimePartNode) {\n    visit(node);\n    if (!pathsEqual(node.path, root.path) && isMessageNode(node)) return;\n    for (const child of node.children) walk(child);\n  }\n\n  walk(root);\n}\n\nfunction walkMimeTree(node: MimePartNode, visit: (node: MimePartNode) => void) {\n  visit(node);\n  for (const child of node.children) walkMimeTree(child, visit);\n}\n\nfunction isEmailAttachmentSidebarNode(\n  node: MimePartNode,\n  bodyNode: MimePartNode | null,\n) {\n  if (node === bodyNode) return false;\n  if (isMultipartNode(node) || isInlineResourceNode(node)) return false;\n  if (isMessageNode(node) || isAttachmentNode(node)) return true;\n  return isRenderableNode(node) && !isBodyMime(node.facts.mimeType);\n}\n\nfunction createSidebarItem({\n  inlineResourceUrls,\n  kind,\n  node,\n  selectedPath,\n}: {\n  inlineResourceUrls: ReadonlyMap<string, string>;\n  kind: \"body\" | \"attachment\";\n  node: MimePartNode;\n  selectedPath: MimePartPath;\n}): EmailSidebarItem {\n  const common = {\n    id: node.path.join(\"/\"),\n    node,\n    path: node.path,\n    description: describeEmailSidebarNode(node),\n    thumbnail: deriveEmailSidebarThumbnail(node, inlineResourceUrls),\n    isSelected: pathsEqual(node.path, selectedPath),\n  };\n\n  if (kind === \"body\") {\n    return {\n      ...common,\n      kind,\n      title: \"Body\",\n    };\n  }\n\n  return {\n    ...common,\n    kind,\n    title: mimePartLabel(node.part),\n  };\n}\n\nfunction deriveEmailSidebarThumbnail(\n  node: MimePartNode,\n  inlineResourceUrls: ReadonlyMap<string, string>,\n): EmailSidebarThumbnailModel {\n  const thumbnailNode = isMessageNode(node)\n    ? selectMimeScopeBodyNode(node)\n    : node;\n\n  if (thumbnailNode?.part.source && !isInlineResourceNode(thumbnailNode)) {\n    return {\n      kind: \"file\",\n      source: resolveEmailPreviewSource(thumbnailNode, inlineResourceUrls),\n      aspectRatio: 1,\n    };\n  }\n\n  if (isMessageNode(node)) return { kind: \"icon\", icon: \"mail\" };\n  if (isMultipartNode(node)) return { kind: \"icon\", icon: \"layers\" };\n  if (isAttachmentNode(node)) return { kind: \"icon\", icon: \"paperclip\" };\n  return { kind: \"icon\", icon: \"file\" };\n}\n\nfunction describeEmailSidebarNode(node: MimePartNode) {\n  if (node.part.size != null) {\n    return `${node.part.mimeType} · ${formatFileSize(node.part.size)}`;\n  }\n  if (isInlineResourceNode(node)) return `${node.part.mimeType} · inline`;\n  if (isAttachmentNode(node)) return `${node.part.mimeType} · attachment`;\n  return node.part.mimeType;\n}\n\nfunction deriveNestedEmailMessage(\n  message: EmailViewerMessage,\n  node: MimePartNode,\n): EmailViewerMessage {\n  return {\n    id: `${messageIdentity(message)}:${node.path.join(\"/\")}`,\n    headers: node.part.headers,\n    subject: headerValue(node.part.headers, \"subject\"),\n    from: headerValue(node.part.headers, \"from\"),\n    to: headerValue(node.part.headers, \"to\"),\n    cc: headerValue(node.part.headers, \"cc\"),\n    bcc: headerValue(node.part.headers, \"bcc\"),\n    sentAt: headerValue(node.part.headers, \"date\"),\n    root: node.part,\n  };\n}\n\nfunction resolveEmailPreviewSource(\n  node: MimePartNode,\n  inlineResourceUrls: ReadonlyMap<string, string>,\n): ViewerSource {\n  const source = node.part.source;\n  if (source?.kind === \"text\" && isHtmlMime(node.part)) {\n    const text = replaceInlineResourceUrls(source.text, inlineResourceUrls);\n    if (text === source.text) return source;\n\n    return {\n      ...source,\n      identityKey: [\n        source.identityKey ?? node.path.join(\"/\"),\n        \"email-inline\",\n        inlineResourceIdentity(inlineResourceUrls),\n      ].join(\":\"),\n      text,\n    };\n  }\n\n  return source!;\n}\n\nfunction createEmptyContent({\n  message,\n  node,\n  reason,\n}: {\n  message: string;\n  node: MimePartNode;\n  reason: EmailContentEmptyReason;\n}): EmailContentModel {\n  return {\n    kind: \"empty\",\n    message,\n    node,\n    reason,\n  };\n}\n\nfunction normalizeAddressList(\n  value: string | readonly string[] | null | undefined,\n): readonly EmailAddress[] {\n  if (typeof value === \"string\") return parseAddressList(value);\n  if (!value) return [];\n  return value.flatMap((address) => parseAddressList(address));\n}\n\nfunction parseAddressList(value: string): readonly EmailAddress[] {\n  return value\n    .split(/,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/)\n    .map((address) => parseEmailAddress(address))\n    .filter((address) => address.display.length > 0);\n}\n\nfunction parseEmailAddress(value: string): EmailAddress {\n  const display = value.trim();\n  const match = display.match(/^(.*?)<([^>]+)>$/);\n  if (!match) {\n    return {\n      name: null,\n      address: display.includes(\"@\") ? display : null,\n      display,\n    };\n  }\n\n  const name = cleanAddressName(match[1]);\n  const address = match[2]?.trim() || null;\n  return {\n    name,\n    address,\n    display,\n  };\n}\n\nfunction cleanAddressName(value: string | undefined) {\n  const trimmed = value?.trim().replace(/^\"|\"$/g, \"\") ?? \"\";\n  return trimmed || null;\n}\n\nfunction formatSentAt(value: string | Date | null | undefined) {\n  if (!value) return null;\n  const date = value instanceof Date ? value : new Date(value);\n  if (Number.isNaN(date.getTime())) return null;\n  return new Intl.DateTimeFormat(undefined, {\n    dateStyle: \"medium\",\n    timeStyle: \"short\",\n  }).format(date);\n}\n\nfunction headerValue(\n  headers: readonly { name: string; value: string }[] | undefined,\n  name: string,\n) {\n  return (\n    headers?.find((header) => header.name.toLowerCase() === name.toLowerCase())\n      ?.value ?? null\n  );\n}\n\nfunction contentLocationForPart(part: MimePart) {\n  return normalizeContentLocation(\n    part.contentLocation ?? headerValue(part.headers, \"content-location\"),\n  );\n}\n\nfunction inlineResourceKeysForNode(\n  node: MimePartNode,\n): readonly EmailInlineResourceKey[] {\n  const keys: EmailInlineResourceKey[] = [];\n  if (node.facts.contentId) {\n    keys.push({ kind: \"content-id\", value: node.facts.contentId });\n  }\n  if (node.facts.contentLocation) {\n    keys.push({ kind: \"content-location\", value: node.facts.contentLocation });\n  }\n  return keys;\n}\n\nfunction replaceContentLocationUrls(\n  html: string,\n  inlineUrls: ReadonlyMap<string, string>,\n) {\n  return html.replace(\n    /\\b(src|href)=([\"'])([^\"']+)\\2/gi,\n    (match, attribute, quote, rawUrl) => {\n      if (!isRelativeInlineReference(rawUrl)) return match;\n\n      const normalized = normalizeRelativeReference(rawUrl);\n      const url = inlineUrls.get(\n        inlineResourceKeyToString({\n          kind: \"content-location\",\n          value: normalized,\n        }),\n      );\n      return url ? `${attribute}=${quote}${url}${quote}` : match;\n    },\n  );\n}\n\nfunction isRelativeInlineReference(value: string) {\n  const lower = value.trim().toLowerCase();\n  if (!lower) return false;\n  if (lower.startsWith(\"#\")) return false;\n  if (lower.startsWith(\"/\")) return false;\n  if (lower.startsWith(\"cid:\")) return false;\n  if (lower.startsWith(\"data:\")) return false;\n  return !/^[a-z][a-z0-9+.-]*:/i.test(lower);\n}\n\nfunction normalizeRelativeReference(value: string) {\n  return value\n    .trim()\n    .replace(/^\\.\\/+/, \"\")\n    .replace(/\\\\/g, \"/\")\n    .toLowerCase();\n}\n\nfunction getParentMimeNode(rootNode: MimePartNode, node: MimePartNode) {\n  return node.parentPath ? findMimeNodeByPath(rootNode, node.parentPath) : null;\n}\n\nfunction inlineResourceIdentity(\n  inlineResourceUrls: ReadonlyMap<string, string>,\n) {\n  return Array.from(inlineResourceUrls.entries())\n    .sort(([left], [right]) => left.localeCompare(right))\n    .map(([key, url]) => `${key}=${url}`)\n    .join(\"|\");\n}\n\nfunction decodeCid(value: string) {\n  try {\n    return decodeURIComponent(value);\n  } catch {\n    return value;\n  }\n}\n\nfunction isHtmlMime(part: MimePart) {\n  return normalizedMimeType(part.mimeType) === \"text/html\";\n}\n\nfunction isBodyMime(mimeType: string) {\n  return (\n    mimeType === \"text/html\" ||\n    mimeType === \"text/plain\" ||\n    mimeType === \"text/markdown\"\n  );\n}\n\nfunction isMultipartMime(mimeType: string) {\n  return normalizedMimeType(mimeType).startsWith(\"multipart/\");\n}\n\nfunction isRelatedMultipart(mimeType: string) {\n  return normalizedMimeType(mimeType) === \"multipart/related\";\n}\n\nfunction isMessageMime(mimeType: string) {\n  const mime = normalizedMimeType(mimeType);\n  return mime === \"message/rfc822\" || mime === \"message/global\";\n}\n\nfunction isDeliveryStatusMime(mimeType: string) {\n  const mime = normalizedMimeType(mimeType);\n  return (\n    mime === \"message/delivery-status\" ||\n    mime === \"message/disposition-notification\"\n  );\n}\n\nfunction isSecurityEnvelopeMime(mimeType: string) {\n  const mime = normalizedMimeType(mimeType);\n  return (\n    mime === \"application/pkcs7-mime\" ||\n    mime === \"application/pgp-encrypted\" ||\n    mime === \"multipart/encrypted\"\n  );\n}\n\nfunction securityEnvelopeLabel(mimeType: string) {\n  const mime = normalizedMimeType(mimeType);\n  if (mime.includes(\"encrypted\")) return \"Encrypted message\";\n  if (mime.includes(\"pkcs7\")) return \"Signed or encrypted message\";\n  return \"Security envelope\";\n}\n\nfunction normalizedMimeType(mimeType: string) {\n  return mimeType.toLowerCase().split(\";\")[0].trim();\n}\n\nfunction multipartLabel(mimeType: string) {\n  const subtype = normalizedMimeType(mimeType).split(\"/\")[1];\n  if (!subtype) return \"Multipart\";\n  return `Multipart ${subtype}`;\n}\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer-model.ts"
    },
    {
      "path": "registry/new-york-v4/ui/email-viewer-inline-resources.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { ViewerSource } from \"@/lib/viewer-source\";\n\nimport {\n  inlineResourceKeyToString,\n  MISSING_EMAIL_INLINE_RESOURCE_URL,\n} from \"./email-viewer-model\";\nimport type { EmailInlineResourceScope } from \"./email-viewer-types\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport function useEmailInlineResourceUrls(scope: EmailInlineResourceScope) {\n  const placeholderUrls = React.useMemo(\n    () => createPlaceholderUrls(scope),\n    [scope],\n  );\n  const [materialized, setMaterialized] = React.useState<{\n    scope: EmailInlineResourceScope;\n    urls: ReadonlyMap<string, string>;\n  }>(() => ({\n    scope,\n    urls: placeholderUrls,\n  }));\n\n  useKeyedMountEffect(joinEffectKey([scope]), () => {\n    const nextUrls = new Map<string, string>();\n    const objectUrls: string[] = [];\n\n    for (const resource of scope.resources) {\n      const source = resource.node.part.source;\n      if (!source) continue;\n\n      const url = sourceToInlineUrl(source, objectUrls);\n      if (!url) continue;\n\n      for (const key of resource.keys) {\n        nextUrls.set(inlineResourceKeyToString(key), url);\n      }\n    }\n\n    setMaterialized({\n      scope,\n      urls: nextUrls,\n    });\n\n    return () => {\n      for (const url of objectUrls) URL.revokeObjectURL(url);\n    };\n  });\n\n  return materialized.scope === scope ? materialized.urls : placeholderUrls;\n}\n\nfunction createPlaceholderUrls(scope: EmailInlineResourceScope) {\n  const urls = new Map<string, string>();\n\n  for (const resource of scope.resources) {\n    for (const key of resource.keys) {\n      urls.set(\n        inlineResourceKeyToString(key),\n        MISSING_EMAIL_INLINE_RESOURCE_URL,\n      );\n    }\n  }\n\n  return urls;\n}\n\nfunction sourceToInlineUrl(source: ViewerSource, objectUrls: string[]) {\n  if (source.kind === \"url\") return source.url;\n  if (source.kind === \"blob\") {\n    const url = URL.createObjectURL(source.blob);\n    objectUrls.push(url);\n    return url;\n  }\n\n  return textSourceToDataUrl(source.text, source.mimeType);\n}\n\nfunction textSourceToDataUrl(text: string, mimeType: string | undefined) {\n  const bytes = new TextEncoder().encode(text);\n  const chunkSize = 0x8000;\n  let binary = \"\";\n  for (let index = 0; index < bytes.length; index += chunkSize) {\n    binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));\n  }\n  return `data:${mimeType ?? \"text/plain;charset=utf-8\"};base64,${btoa(binary)}`;\n}\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer-inline-resources.ts"
    },
    {
      "path": "registry/new-york-v4/ui/email-viewer-types.ts",
      "content": "import type { ReactNode } from \"react\";\n\nimport type { FileCategory, ViewerSource } from \"@/lib/viewer-source\";\n\nexport type MimePartDisposition = \"inline\" | \"attachment\" | string;\n\nexport type MimeHeader = {\n  name: string;\n  value: string;\n};\n\nexport type MimePart = {\n  id: string;\n  mimeType: string;\n  headers?: readonly MimeHeader[];\n  fileName?: string | null;\n  disposition?: MimePartDisposition | null;\n  contentId?: string | null;\n  contentLocation?: string | null;\n  size?: number | null;\n  source?: ViewerSource;\n  children?: readonly MimePart[];\n};\n\nexport type MimeMessage = {\n  id?: string;\n  headers?: readonly MimeHeader[];\n  subject?: string | null;\n  from?: string | readonly string[] | null;\n  to?: string | readonly string[] | null;\n  cc?: string | readonly string[] | null;\n  bcc?: string | readonly string[] | null;\n  sentAt?: string | Date | null;\n  root: MimePart;\n};\n\nexport type MimePartPath = readonly string[];\n\nexport type MimePartKind =\n  | \"multipart\"\n  | \"message\"\n  | \"body\"\n  | \"attachment\"\n  | \"inline-resource\"\n  | \"unsupported\";\n\nexport type MimePreviewPolicy =\n  | { kind: \"preview\"; category?: FileCategory }\n  | { kind: \"nested-message\" }\n  | { kind: \"attachment\"; category?: FileCategory }\n  | { kind: \"security-envelope\"; label: string }\n  | { kind: \"unsupported\"; reason: EmailContentEmptyReason };\n\nexport type MimePartFacts = {\n  kind: MimePartKind;\n  mimeType: string;\n  disposition: string | null;\n  contentId: string | null;\n  contentLocation: string | null;\n  isRenderable: boolean;\n  preview: MimePreviewPolicy;\n};\n\nexport type MimePartNode = {\n  part: MimePart;\n  path: MimePartPath;\n  parentPath: MimePartPath | null;\n  depth: number;\n  children: readonly MimePartNode[];\n  facts: MimePartFacts;\n};\n\nexport type EmailViewerMessage = MimeMessage;\n\nexport type MimeMessageScope = {\n  message: EmailViewerMessage;\n  root: MimePartNode;\n  path: MimePartPath;\n  descendants: readonly MimePartNode[];\n};\n\nexport type EmailAddress = {\n  name: string | null;\n  address: string | null;\n  display: string;\n};\n\nexport type EmailHeaderModel = {\n  subject: string;\n  from: readonly EmailAddress[];\n  to: readonly EmailAddress[];\n  cc: readonly EmailAddress[];\n  bcc: readonly EmailAddress[];\n  sentAt: string | null;\n};\n\nexport type EmailSidebarThumbnailModel =\n  | { kind: \"file\"; source: ViewerSource; aspectRatio: number }\n  | { kind: \"icon\"; icon: \"file\" | \"layers\" | \"mail\" | \"paperclip\" };\n\nexport type EmailSidebarItemBase = {\n  id: string;\n  node: MimePartNode;\n  path: MimePartPath;\n  description: string;\n  thumbnail: EmailSidebarThumbnailModel;\n  isSelected: boolean;\n};\n\nexport type EmailBodySidebarItem = EmailSidebarItemBase & {\n  kind: \"body\";\n  title: \"Body\";\n};\n\nexport type EmailAttachmentSidebarItem = EmailSidebarItemBase & {\n  kind: \"attachment\";\n  title: string;\n};\n\nexport type EmailSidebarItem =\n  | EmailBodySidebarItem\n  | EmailAttachmentSidebarItem;\n\nexport type EmailSidebarSection = {\n  id: \"body\" | \"attachments\";\n  title: string;\n  items: readonly EmailSidebarItem[];\n  emptyLabel?: string;\n};\n\nexport type EmailSidebarModel = {\n  bodyCount: number;\n  attachmentCount: number;\n  sections: readonly EmailSidebarSection[];\n};\n\nexport type EmailFilePayload = {\n  source: ViewerSource;\n  category?: FileCategory;\n};\n\nexport type EmailContentFile = {\n  kind: \"file\";\n  node: MimePartNode;\n  file: EmailFilePayload;\n};\n\nexport type EmailContentNestedMessage = {\n  kind: \"nested-message\";\n  node: MimePartNode;\n  message: EmailViewerMessage;\n  maxNestedMessageDepth: number;\n  nestedMessageDepth: number;\n};\n\nexport type EmailContentEmptyReason =\n  | \"no-previewable-body\"\n  | \"unsupported-part\"\n  | \"missing-source\"\n  | \"nested-depth-exceeded\"\n  | \"security-envelope\";\n\nexport type EmailContentEmpty = {\n  kind: \"empty\";\n  reason: EmailContentEmptyReason;\n  node: MimePartNode;\n  message: string;\n};\n\nexport type EmailContentModel =\n  | EmailContentFile\n  | EmailContentNestedMessage\n  | EmailContentEmpty;\n\nexport type EmailInlineResourceKey =\n  | { kind: \"content-id\"; value: string }\n  | { kind: \"content-location\"; value: string };\n\nexport type EmailInlineResource = {\n  node: MimePartNode;\n  keys: readonly EmailInlineResourceKey[];\n};\n\nexport type EmailInlineResourceScope = {\n  root: MimePartNode;\n  resources: readonly EmailInlineResource[];\n};\n\nexport type EmailBodySelectionPolicy = {\n  preferredMimeTypes: readonly string[];\n  includeInlineBodyParts: boolean;\n  includeAttachments: boolean;\n};\n\nexport type EmailViewerModel = {\n  message: EmailViewerMessage;\n  rootNode: MimePartNode;\n  scope: MimeMessageScope;\n  selectedPath: MimePartPath;\n  selectedNode: MimePartNode;\n  header: EmailHeaderModel;\n  sidebar: EmailSidebarModel;\n  content: EmailContentModel;\n};\n\nexport type EmailViewerProps = {\n  message: EmailViewerMessage;\n  selectedPath?: MimePartPath | null;\n  defaultSelectedPath?: MimePartPath;\n  onSelectedPathChange?: (path: MimePartPath, node: MimePartNode) => void;\n  maxNestedMessageDepth?: number;\n  mode?: \"auto\" | \"inline\" | \"overlay\";\n  className?: string;\n};\n\nexport type EmailViewerProviderProps = Omit<\n  EmailViewerProps,\n  \"className\" | \"mode\"\n> & {\n  children: ReactNode;\n};\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer-types.ts"
    },
    {
      "path": "registry/new-york-v4/ui/email-viewer-content.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  viewerContentRenderKey,\n  type ViewerResource,\n} from \"@/lib/viewer-resource\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nimport { EmailViewer } from \"./email-viewer\";\nimport { parseEmlMessage } from \"./email-viewer-eml\";\nimport { ViewerFallback } from \"./file-viewer-fallback\";\nimport { loadTextResource } from \"./file-viewer-text-resource\";\nimport { isAbortError } from \"./viewer-abortable-request\";\nimport {\n  useViewerControlsRegistration,\n  type ViewerControlsState,\n} from \"./viewer-controls\";\n\nexport type EmailResourceContentProps = {\n  resource: ViewerResource;\n  className?: string;\n  bare?: boolean;\n  controls?: boolean;\n  download?: boolean;\n  descriptorSignal?: AbortSignal;\n};\n\ntype EmailLoadState =\n  | { status: \"loading\"; key: unknown }\n  | { status: \"loaded\"; key: unknown; emlText: string }\n  | { status: \"error\"; key: unknown; error: unknown };\n\nexport function EmailResourceContent({\n  resource,\n  className,\n  bare,\n  controls = true,\n  download = true,\n  descriptorSignal,\n}: EmailResourceContentProps) {\n  if (resource.content.payload.kind === \"text\") {\n    return (\n      <EmailResourceFrame\n        key={viewerContentRenderKey(resource.content)}\n        resource={resource}\n        emlText={resource.content.payload.text}\n        className={className}\n        bare={bare}\n        controls={controls}\n        download={download}\n      />\n    );\n  }\n  return (\n    <EmailResourceLoader\n      resource={resource}\n      className={className}\n      bare={bare}\n      controls={controls}\n      download={download}\n      descriptorSignal={descriptorSignal}\n    />\n  );\n}\n\nfunction EmailResourceLoader({\n  resource,\n  className,\n  bare,\n  controls,\n  download,\n  descriptorSignal,\n}: {\n  resource: ViewerResource;\n  className?: string;\n  bare?: boolean;\n  controls: boolean;\n  download: boolean;\n  descriptorSignal?: AbortSignal;\n}) {\n  const content = resource.content;\n  const contentKey = content.key;\n  const [state, setState] = React.useState<EmailLoadState>({\n    status: \"loading\",\n    key: contentKey,\n  });\n\n  useKeyedMountEffect(\n    joinEffectKey([content, contentKey, descriptorSignal, resource.fileName]),\n    () => {\n      let active = true;\n      const controller = new AbortController();\n      const abortLocal = () => controller.abort();\n      setState({ status: \"loading\", key: contentKey });\n\n      if (descriptorSignal?.aborted) {\n        abortLocal();\n      } else {\n        descriptorSignal?.addEventListener(\"abort\", abortLocal, {\n          once: true,\n        });\n      }\n\n      loadTextResource({\n        content,\n        fileName: resource.fileName,\n        signal: controller.signal,\n      }).then(\n        (emlText) => {\n          if (active && !controller.signal.aborted) {\n            setState({ status: \"loaded\", key: contentKey, emlText });\n          }\n        },\n        (error: unknown) => {\n          if (!active || isAbortError(error)) return;\n          setState({ status: \"error\", key: contentKey, error });\n        },\n      );\n\n      return () => {\n        active = false;\n        descriptorSignal?.removeEventListener(\"abort\", abortLocal);\n        abortLocal();\n      };\n    },\n  );\n\n  if (state.key !== contentKey || state.status === \"loading\") {\n    return (\n      <ViewerFallback resource={resource} className={className} bare={bare} />\n    );\n  }\n  if (state.status === \"error\") {\n    throw state.error;\n  }\n\n  return (\n    <EmailResourceFrame\n      key={contentKey}\n      resource={resource}\n      emlText={state.emlText}\n      className={className}\n      bare={bare}\n      controls={controls}\n      download={download}\n    />\n  );\n}\n\nfunction EmailResourceFrame({\n  resource,\n  emlText,\n  className,\n  bare,\n  download,\n}: {\n  resource: ViewerResource;\n  emlText: string;\n  className?: string;\n  bare?: boolean;\n  controls: boolean;\n  download: boolean;\n}) {\n  const message = React.useMemo(\n    () => parseEmlMessage(emlText, { identityKey: resource.content.key }),\n    [emlText, resource.content.key],\n  );\n  useEmailResourceControlsRegistration({\n    download,\n    downloadAction: resource.originalDownload,\n  });\n\n  // The email viewer supplies its own domain chrome (message header, parts\n  // sidebar); it tracks the live layout like the csv/html renderers, so it\n  // registers no motion resolver — the kernel's identity default is correct.\n  return (\n    <div\n      data-slot=\"email-file-viewer-content\"\n      className={cn(\n        \"bg-background flex min-h-0 flex-1 flex-col overflow-hidden\",\n        bare ? \"h-full\" : \"min-h-64\",\n        className,\n      )}\n    >\n      <EmailViewer message={message} className=\"min-h-0 flex-1\" />\n    </div>\n  );\n}\n\nfunction useEmailResourceControlsRegistration({\n  download,\n  downloadAction,\n}: {\n  download: boolean;\n  downloadAction: ViewerResource[\"originalDownload\"];\n}) {\n  const onControlsChange = useViewerControlsRegistration();\n  const controlsState = React.useMemo<ViewerControlsState>(\n    () => ({\n      downloads: download && downloadAction ? [downloadAction] : [],\n    }),\n    [download, downloadAction],\n  );\n\n  useKeyedMountEffect(\n    joinEffectKey([\"email-controls\", onControlsChange, controlsState]),\n    () => {\n      if (!onControlsChange) return;\n      onControlsChange(controlsState);\n      return () => onControlsChange(null);\n    },\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer-content.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/email-viewer-eml.ts",
      "content": "import { blobSource } from \"@/lib/viewer-resource\";\nimport { textPayloadKey, type ViewerSource } from \"@/lib/viewer-source\";\n\nimport type {\n  EmailViewerMessage,\n  MimeHeader,\n  MimePart,\n} from \"./email-viewer-types\";\n\n/**\n * Lenient RFC 822/2045 `.eml` parser producing the `EmailViewerMessage` shape\n * consumed by `EmailViewer`. It never throws on malformed input: unparseable\n * structure degrades to a single text part so the viewer still renders.\n */\nexport type ParseEmlOptions = {\n  /**\n   * Stable identity prefix for derived part sources. Defaults to a content\n   * hash of the message text so re-parsing the same bytes interns to the\n   * same viewer resources.\n   */\n  identityKey?: string;\n};\n\nconst MAX_MIME_DEPTH = 24;\nconst LINE_ENDING_PATTERN = /\\r\\n?/g;\n\nexport function parseEmlMessage(\n  emlText: string,\n  options: ParseEmlOptions = {},\n): EmailViewerMessage {\n  const text = emlText.replace(LINE_ENDING_PATTERN, \"\\n\");\n  const identityBase = options.identityKey ?? `eml-${textPayloadKey(text)}`;\n  const { headers, body } = splitEntity(text);\n  const root = parseEntityPart({\n    headers,\n    body,\n    id: \"root\",\n    identityBase,\n    depth: 0,\n  });\n\n  return {\n    id: identityBase,\n    headers,\n    subject: decodedHeader(headers, \"subject\"),\n    from: decodedHeader(headers, \"from\"),\n    to: decodedHeader(headers, \"to\"),\n    cc: decodedHeader(headers, \"cc\"),\n    bcc: decodedHeader(headers, \"bcc\"),\n    sentAt: headerValue(headers, \"date\"),\n    root,\n  };\n}\n\ntype MimeEntity = {\n  headers: readonly MimeHeader[];\n  body: string;\n};\n\ntype ParseEntityInput = MimeEntity & {\n  id: string;\n  identityBase: string;\n  depth: number;\n};\n\nfunction parseEntityPart({\n  headers,\n  body,\n  id,\n  identityBase,\n  depth,\n}: ParseEntityInput): MimePart {\n  const contentType = parseParameterizedHeader(\n    headerValue(headers, \"content-type\") ?? \"text/plain\",\n  );\n  const mimeType = contentType.value || \"text/plain\";\n  const disposition = parseParameterizedHeader(\n    headerValue(headers, \"content-disposition\") ?? \"\",\n  );\n  const fileName =\n    readNameParameter(disposition.params, \"filename\") ??\n    readNameParameter(contentType.params, \"name\");\n  const base: MimePart = {\n    id,\n    mimeType,\n    headers,\n    fileName: fileName ?? null,\n    disposition: disposition.value || null,\n    contentId: headerValue(headers, \"content-id\"),\n    contentLocation: headerValue(headers, \"content-location\"),\n  };\n  const encoding = (headerValue(headers, \"content-transfer-encoding\") ?? \"\")\n    .trim()\n    .toLowerCase();\n\n  if (depth < MAX_MIME_DEPTH) {\n    const boundary = contentType.params[\"boundary\"];\n    if (mimeType.startsWith(\"multipart/\") && boundary) {\n      const sections = splitMultipartSections(body, boundary);\n      if (sections.length > 0) {\n        return {\n          ...base,\n          children: sections.map((section, index) => {\n            const entity = splitEntity(section);\n            return parseEntityPart({\n              ...entity,\n              id: `${id}.${index + 1}`,\n              identityBase,\n              depth: depth + 1,\n            });\n          }),\n        };\n      }\n    }\n\n    if (mimeType === \"message/rfc822\" || mimeType === \"message/global\") {\n      const nestedText = decodeBodyToText(body, encoding, \"utf-8\");\n      const nested = splitEntity(nestedText);\n      const nestedRoot = parseEntityPart({\n        ...nested,\n        id: `${id}.m`,\n        identityBase,\n        depth: depth + 1,\n      });\n      return {\n        ...base,\n        // Nested message headers (Subject/From/…) ride on the rfc822 part so\n        // the email model can derive the nested viewer's message from it.\n        headers: [...headers, ...nested.headers],\n        children: [nestedRoot],\n      };\n    }\n  }\n\n  return {\n    ...base,\n    ...leafSource({\n      body,\n      encoding,\n      fileName,\n      id,\n      identityBase,\n      mimeType,\n      charset: contentType.params[\"charset\"],\n    }),\n  };\n}\n\nfunction leafSource({\n  body,\n  charset,\n  encoding,\n  fileName,\n  id,\n  identityBase,\n  mimeType,\n}: {\n  body: string;\n  charset: string | undefined;\n  encoding: string;\n  fileName: string | null | undefined;\n  id: string;\n  identityBase: string;\n  mimeType: string;\n}): Pick<MimePart, \"source\" | \"size\"> {\n  const identityKey = `${identityBase}:${id}`;\n  const resolvedFileName = fileName ?? defaultLeafFileName(mimeType);\n\n  if (isTextualMime(mimeType)) {\n    const text = decodeBodyToText(body, encoding, charset);\n    const source: ViewerSource = {\n      kind: \"text\",\n      text,\n      fileName: resolvedFileName,\n      mimeType,\n      identityKey,\n    };\n    return { source, size: text.length };\n  }\n\n  const bytes = decodeBodyToBytes(body, encoding);\n  return {\n    source: blobSource(bytes, {\n      identityKey,\n      fileName: resolvedFileName,\n      mimeType,\n    }),\n    size: bytes.byteLength,\n  };\n}\n\nfunction isTextualMime(mimeType: string) {\n  return (\n    mimeType.startsWith(\"text/\") ||\n    mimeType === \"application/json\" ||\n    mimeType === \"application/xml\" ||\n    mimeType === \"image/svg+xml\" ||\n    mimeType.startsWith(\"message/\")\n  );\n}\n\nfunction defaultLeafFileName(mimeType: string) {\n  if (mimeType === \"text/html\") return \"message.html\";\n  if (mimeType === \"text/plain\") return \"message.txt\";\n  return undefined;\n}\n\n// --- entity + header parsing ---------------------------------------------\n\nfunction splitEntity(text: string): MimeEntity {\n  const trimmed = text.startsWith(\"\\n\") ? text.slice(1) : text;\n  if (trimmed !== text) {\n    // Entity starts with a blank line: empty header block, all body.\n    return { headers: [], body: trimmed };\n  }\n  const separator = text.indexOf(\"\\n\\n\");\n  if (separator === -1) {\n    // No blank line: treat a colon-bearing block as headers-only, otherwise\n    // as a bare body with no headers.\n    if (looksLikeHeaderBlock(text)) {\n      return { headers: parseHeaderBlock(text), body: \"\" };\n    }\n    return { headers: [], body: text };\n  }\n  const headerBlock = text.slice(0, separator);\n  if (!looksLikeHeaderBlock(headerBlock)) {\n    return { headers: [], body: text };\n  }\n  return {\n    headers: parseHeaderBlock(headerBlock),\n    body: text.slice(separator + 2),\n  };\n}\n\nfunction looksLikeHeaderBlock(block: string) {\n  const firstLine = block.slice(0, block.indexOf(\"\\n\") + 1 || undefined);\n  return /^[!-9;-~]+:/.test(firstLine);\n}\n\nfunction parseHeaderBlock(block: string): MimeHeader[] {\n  const unfolded: string[] = [];\n  for (const line of block.split(\"\\n\")) {\n    if ((line.startsWith(\" \") || line.startsWith(\"\\t\")) && unfolded.length) {\n      unfolded[unfolded.length - 1] += ` ${line.trim()}`;\n      continue;\n    }\n    unfolded.push(line);\n  }\n\n  const headers: MimeHeader[] = [];\n  for (const line of unfolded) {\n    const colon = line.indexOf(\":\");\n    if (colon <= 0) continue;\n    headers.push({\n      name: line.slice(0, colon).trim(),\n      value: line.slice(colon + 1).trim(),\n    });\n  }\n  return headers;\n}\n\nfunction headerValue(\n  headers: readonly MimeHeader[],\n  name: string,\n): string | null {\n  const lower = name.toLowerCase();\n  for (const header of headers) {\n    if (header.name.toLowerCase() === lower) return header.value;\n  }\n  return null;\n}\n\nfunction decodedHeader(headers: readonly MimeHeader[], name: string) {\n  const value = headerValue(headers, name);\n  return value == null ? null : decodeEncodedWords(value);\n}\n\ntype ParameterizedHeader = {\n  value: string;\n  params: Record<string, string>;\n};\n\nfunction parseParameterizedHeader(raw: string): ParameterizedHeader {\n  const [first, ...rest] = splitOutsideQuotes(raw, \";\");\n  const params: Record<string, string> = {};\n  const continuations = new Map<string, Map<number, string>>();\n\n  for (const segment of rest) {\n    const equals = segment.indexOf(\"=\");\n    if (equals === -1) continue;\n    const rawName = segment.slice(0, equals).trim().toLowerCase();\n    let value = segment.slice(equals + 1).trim();\n    if (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2) {\n      value = value.slice(1, -1).replace(/\\\\(.)/g, \"$1\");\n    }\n\n    const continuation = rawName.match(/^(.*)\\*(\\d+)(\\*)?$/);\n    if (continuation) {\n      const [, baseName, indexText, extended] = continuation;\n      const index = Number(indexText);\n      let sections = continuations.get(baseName);\n      if (!sections) {\n        sections = new Map();\n        continuations.set(baseName, sections);\n      }\n      sections.set(\n        index,\n        extended && index === 0 ? decodeRfc2231Value(value) : decodePercents(value, extended != null),\n      );\n      continue;\n    }\n\n    if (rawName.endsWith(\"*\")) {\n      params[rawName.slice(0, -1)] = decodeRfc2231Value(value);\n      continue;\n    }\n\n    params[rawName] = value;\n  }\n\n  for (const [name, sections] of continuations) {\n    const ordered = [...sections.entries()].sort((a, b) => a[0] - b[0]);\n    params[name] = ordered.map(([, section]) => section).join(\"\");\n  }\n\n  return { value: (first ?? \"\").trim().toLowerCase(), params };\n}\n\nfunction decodeRfc2231Value(value: string) {\n  const match = value.match(/^([^']*)'[^']*'([\\s\\S]*)$/);\n  if (!match) return decodePercents(value, true);\n  const [, charset, encoded] = match;\n  return decodePercentsWithCharset(encoded, charset || \"utf-8\");\n}\n\nfunction decodePercents(value: string, extended: boolean) {\n  return extended ? decodePercentsWithCharset(value, \"utf-8\") : value;\n}\n\nfunction decodePercentsWithCharset(value: string, charset: string) {\n  const bytes: number[] = [];\n  const encoder = new TextEncoder();\n  for (let index = 0; index < value.length; index += 1) {\n    const char = value[index];\n    if (char === \"%\" && /^[0-9A-Fa-f]{2}$/.test(value.slice(index + 1, index + 3))) {\n      bytes.push(Number.parseInt(value.slice(index + 1, index + 3), 16));\n      index += 2;\n      continue;\n    }\n    bytes.push(...encoder.encode(char));\n  }\n  return decodeBytes(new Uint8Array(bytes), charset);\n}\n\nfunction splitOutsideQuotes(value: string, separator: string) {\n  const segments: string[] = [];\n  let current = \"\";\n  let inQuotes = false;\n  for (let index = 0; index < value.length; index += 1) {\n    const char = value[index];\n    if (char === '\"' && value[index - 1] !== \"\\\\\") inQuotes = !inQuotes;\n    if (char === separator && !inQuotes) {\n      segments.push(current);\n      current = \"\";\n      continue;\n    }\n    current += char;\n  }\n  segments.push(current);\n  return segments;\n}\n\nfunction readNameParameter(\n  params: Record<string, string>,\n  name: string,\n): string | null {\n  const value = params[name];\n  if (!value) return null;\n  return decodeEncodedWords(value);\n}\n\n// --- RFC 2047 encoded words -----------------------------------------------\n\nconst ENCODED_WORD_PATTERN = /=\\?([^?\\s]+)\\?([bq])\\?([^?\\s]*)\\?=/gi;\n\nexport function decodeEncodedWords(value: string): string {\n  // Whitespace between two adjacent encoded words is not significant.\n  const joined = value.replace(/(\\?=)\\s+(=\\?)/g, \"$1$2\");\n  return joined.replace(\n    ENCODED_WORD_PATTERN,\n    (whole, charset: string, encoding: string, data: string) => {\n      try {\n        const bytes =\n          encoding.toLowerCase() === \"b\"\n            ? base64ToBytes(data)\n            : qEncodedToBytes(data);\n        return decodeBytes(bytes, charset);\n      } catch {\n        return whole;\n      }\n    },\n  );\n}\n\nfunction qEncodedToBytes(data: string) {\n  const bytes: number[] = [];\n  for (let index = 0; index < data.length; index += 1) {\n    const char = data[index];\n    if (char === \"_\") {\n      bytes.push(0x20);\n      continue;\n    }\n    if (\n      char === \"=\" &&\n      /^[0-9A-Fa-f]{2}$/.test(data.slice(index + 1, index + 3))\n    ) {\n      bytes.push(Number.parseInt(data.slice(index + 1, index + 3), 16));\n      index += 2;\n      continue;\n    }\n    bytes.push(char.charCodeAt(0) & 0xff);\n  }\n  return new Uint8Array(bytes);\n}\n\n// --- transfer decoding ------------------------------------------------------\n\nfunction decodeBodyToText(\n  body: string,\n  encoding: string,\n  charset: string | undefined,\n) {\n  if (encoding === \"base64\") {\n    try {\n      return decodeBytes(base64ToBytes(body), charset ?? \"utf-8\");\n    } catch {\n      return body;\n    }\n  }\n  if (encoding === \"quoted-printable\") {\n    return decodeBytes(quotedPrintableToBytes(body), charset ?? \"utf-8\");\n  }\n  // 7bit / 8bit / binary / unknown: the surrounding message was already\n  // decoded to a string, so the payload is used as-is.\n  return body;\n}\n\nfunction decodeBodyToBytes(body: string, encoding: string): Uint8Array {\n  if (encoding === \"base64\") {\n    try {\n      return base64ToBytes(body);\n    } catch {\n      return new TextEncoder().encode(body);\n    }\n  }\n  if (encoding === \"quoted-printable\") {\n    return quotedPrintableToBytes(body);\n  }\n  return new TextEncoder().encode(body);\n}\n\nfunction base64ToBytes(data: string) {\n  const clean = data.replace(/[^A-Za-z0-9+/=]/g, \"\");\n  const binary = atob(clean);\n  const bytes = new Uint8Array(binary.length);\n  for (let index = 0; index < binary.length; index += 1) {\n    bytes[index] = binary.charCodeAt(index);\n  }\n  return bytes;\n}\n\nfunction quotedPrintableToBytes(body: string) {\n  // Soft line breaks join the surrounding lines.\n  const text = body.replace(/=\\n/g, \"\");\n  const bytes: number[] = [];\n  const encoder = new TextEncoder();\n  for (let index = 0; index < text.length; index += 1) {\n    const char = text[index];\n    if (\n      char === \"=\" &&\n      /^[0-9A-Fa-f]{2}$/.test(text.slice(index + 1, index + 3))\n    ) {\n      bytes.push(Number.parseInt(text.slice(index + 1, index + 3), 16));\n      index += 2;\n      continue;\n    }\n    bytes.push(...encoder.encode(char));\n  }\n  return new Uint8Array(bytes);\n}\n\nfunction decodeBytes(bytes: Uint8Array, charset: string | undefined) {\n  try {\n    return new TextDecoder(charset || \"utf-8\").decode(bytes);\n  } catch {\n    return new TextDecoder(\"utf-8\").decode(bytes);\n  }\n}\n\n// --- multipart --------------------------------------------------------------\n\nfunction splitMultipartSections(body: string, boundary: string): string[] {\n  const delimiter = `--${boundary}`;\n  const closingDelimiter = `${delimiter}--`;\n  const sections: string[][] = [];\n  let current: string[] | null = null;\n\n  for (const line of body.split(\"\\n\")) {\n    const marker = line.trimEnd();\n    if (marker === closingDelimiter) {\n      if (current) sections.push(current);\n      current = null;\n      break;\n    }\n    if (marker === delimiter) {\n      if (current) sections.push(current);\n      current = [];\n      continue;\n    }\n    current?.push(line);\n  }\n  if (current) sections.push(current);\n\n  return sections.map((lines) => lines.join(\"\\n\"));\n}\n",
      "type": "registry:ui",
      "target": "@ui/email-viewer-eml.ts"
    }
  ],
  "type": "registry:ui"
}