initial commit

This commit is contained in:
rustmailer
2025-11-19 02:14:37 +08:00
commit 1a8f95117e
355 changed files with 54089 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { MailboxData } from "@/api/mailbox/api";
import { TreeDataItem } from "@/components/tree-view";
import { Badge } from '@/components/ui/badge'
import { FolderClosed, FolderOpen } from "lucide-react";
import React from 'react';
type BadgeContentFunction = (item: MailboxData) => React.ReactNode;
export const buildTree = (data: MailboxData[], badgeContent?: BadgeContentFunction, showAttributes?: boolean, showExists?: boolean): TreeDataItem[] => {
const root: TreeDataItem = {
id: 'root',
name: 'Root',
icon: FolderClosed,
openIcon: FolderOpen,
children: [], // Ensure children is initialized as an array
};
const nodeMap: { [key: string]: TreeDataItem } = {};
data.sort((a, b) => a.name.localeCompare(b.name));
data.forEach((item) => {
const { id, name, delimiter, exists, attributes } = item;
const badge = showExists ? (badgeContent ? badgeContent(item) : React.createElement(Badge, {
className: 'text-[12px]',
variant: 'secondary',
}, exists)) : null;
const attributesNode = showAttributes
? attributes.map((item, index) =>
React.createElement(
Badge,
{
key: index,
className: 'text-[12px] mr-1 last:mr-0',
variant: 'secondary',
},
item.attr === 'Extension' ? item.extension || '' : item.attr
)
)
: null;
// const badge = badgeContent || React.createElement(Badge, {
// className: 'text-xs',
// }, exists);
// If there is no delimiter, add the item directly as a child of the root
if (!delimiter) {
root.children!.push({
id: id.toString(),
name,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: undefined, // Leaf node, so children is undefined
});
return;
}
// Split the name into parts based on the delimiter
const parts = name.split(delimiter); // dir/sub1/sub2
let currentParent = root;
// Traverse or create nodes for each part of the path
parts.forEach((part, index) => {
const path = parts.slice(0, index + 1).join(delimiter);
// If the node already exists, set it as the current parent
if (nodeMap[path]) {
currentParent = nodeMap[path];
} else {
// Determine if this is a leaf node (last part of the path)
const isLeaf = index === parts.length - 1;
// Create a new node
const newNode: TreeDataItem = {
id: isLeaf ? id.toString() : path, // Use item.id for leaf nodes, path for non-leaf nodes
name: part,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: isLeaf ? undefined : [], // Ensure children is initialized as an array for non-leaf nodes
};
// Ensure currentParent.children is initialized as an array
if (!currentParent.children) {
currentParent.children = [];
}
// Add the new node to the current parent's children
currentParent.children.push(newNode);
currentParent = newNode; // Update the current parent to the new node
nodeMap[path] = newNode; // Store the node in the map for quick lookup
}
});
});
// Return the children of the root as the final tree structure
return root.children!;
};
+113
View File
@@ -0,0 +1,113 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const formatBytes = (sizeInBytes: number): string => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} B`;
} else if (sizeInBytes < 1024 * 1024) {
return `${(sizeInBytes / 1024).toFixed(2)} KB`;
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
} else {
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
};
export const validateFlag = (input: string): string | null => {
// Check if the string is empty
if (input.length === 0) {
return `'input' cannot be empty.`;
}
// Check if the length is greater than 64 characters
if (input.length > 64) {
return `'input' cannot be longer than 64 characters.`;
}
// Check if the string starts with a letter and contains only letters, numbers, underscores, or dashes
const regex = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
if (!regex.test(input)) {
return `'input' must start with a letter and can only contain letters, numbers, underscores, or dashes.`;
}
// If all checks pass, return null
return null;
};
export function mapToRecordOfArrays(
map: Map<number, Set<number>>
): Record<string, number[]> {
return Object.fromEntries(
Array.from(map.entries()).map(([key, value]) => [String(key), Array.from(value)])
);
}
export function formatNumber(num: number) {
return new Intl.NumberFormat('en-US').format(num);
}
export function validateTag(facetPath: string) {
if (!facetPath || facetPath.length === 0) {
return {
valid: false,
error: "Tag path cannot be empty"
};
}
if (!facetPath.startsWith('/')) {
return {
valid: false,
error: "Tag path must start with '/'"
};
}
let escaped = false;
for (let i = 1; i < facetPath.length; i++) {
const char = facetPath[i];
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
}
}
if (escaped) {
return {
valid: false,
error: "Tag path has unmatched escape character at the end"
};
}
return { valid: true };
}