mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-30 01:44:23 +00:00
update
This commit is contained in:
@@ -29,11 +29,11 @@ use crate::modules::rest::ErrorCode;
|
||||
use crate::modules::store::tantivy::attachment::ATTACHMENT_MANAGER;
|
||||
use crate::modules::store::tantivy::model::AttachmentModel;
|
||||
use crate::modules::users::permissions::Permission;
|
||||
use crate::modules::utils::validate_tag;
|
||||
use crate::raise_error;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::OpenApi;
|
||||
use tantivy::schema::Facet;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct AttachmentApi;
|
||||
@@ -133,8 +133,8 @@ impl AttachmentApi {
|
||||
) -> ApiResult<()> {
|
||||
let req = req.0;
|
||||
for tag in &req.tags {
|
||||
validate_tag(tag)
|
||||
.map_err(|e| raise_error!(format!("{}", e), ErrorCode::InvalidParameter))?;
|
||||
Facet::from_text(tag)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?;
|
||||
}
|
||||
|
||||
for account_id in req.updates.keys() {
|
||||
|
||||
@@ -38,12 +38,12 @@ use crate::modules::store::envelope::Envelope;
|
||||
use crate::modules::store::storage::get_reader;
|
||||
use crate::modules::store::tantivy::envelope::ENVELOPE_MANAGER;
|
||||
use crate::modules::users::permissions::Permission;
|
||||
use crate::modules::utils::validate_tag;
|
||||
use crate::raise_error;
|
||||
use poem::Body;
|
||||
use poem_openapi::param::{Path, Query};
|
||||
use poem_openapi::payload::{Attachment, AttachmentType, Json};
|
||||
use poem_openapi::OpenApi;
|
||||
use tantivy::schema::Facet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -350,8 +350,8 @@ impl MessageApi {
|
||||
) -> ApiResult<()> {
|
||||
let req = req.0;
|
||||
for tag in &req.tags {
|
||||
validate_tag(tag)
|
||||
.map_err(|e| raise_error!(format!("{}", e), ErrorCode::InvalidParameter))?;
|
||||
Facet::from_text(tag)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?;
|
||||
}
|
||||
|
||||
for account_id in req.updates.keys() {
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 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/>.
|
||||
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use tantivy::{schema::Value, TantivyDocument};
|
||||
use tantivy::{
|
||||
schema::{Facet, Value},
|
||||
TantivyDocument,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
modules::{
|
||||
@@ -107,7 +128,12 @@ impl EnvelopeWithAttachments {
|
||||
let tags: Vec<String> = doc
|
||||
.get_all(fields.f_tags)
|
||||
.filter_map(|value| value.as_facet())
|
||||
.map(|f| f.to_string())
|
||||
.map(|facet_encoded_str| {
|
||||
Facet::from_encoded(facet_encoded_str.as_bytes().to_vec())
|
||||
.ok()
|
||||
.map(|facet| facet.to_string())
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let account_id = extract_u64_field(doc, fields.f_account_id, F_ACCOUNT_ID)?;
|
||||
@@ -361,14 +387,25 @@ impl AttachmentModel {
|
||||
let tags: Vec<String> = doc
|
||||
.get_all(f.f_tags)
|
||||
.filter_map(|value| value.as_facet())
|
||||
.map(|f| f.to_string())
|
||||
.map(|facet_encoded_str| {
|
||||
Facet::from_encoded(facet_encoded_str.as_bytes().to_vec())
|
||||
.ok()
|
||||
.map(|facet| facet.to_string())
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let auto_tags: Vec<String> = doc
|
||||
.get_all(f.f_auto_tags)
|
||||
.filter_map(|value| value.as_facet())
|
||||
.map(|f| f.to_string())
|
||||
.map(|facet_encoded_str| {
|
||||
Facet::from_encoded(facet_encoded_str.as_bytes().to_vec())
|
||||
.ok()
|
||||
.map(|facet| facet.to_string())
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let account_id = extract_u64_field(doc, f.f_account_id, F_ACCOUNT_ID)?;
|
||||
let mailbox_id = extract_u64_field(doc, f.f_mailbox_id, F_MAILBOX_ID)?;
|
||||
let account = AccountModel::get(account_id)?;
|
||||
|
||||
@@ -347,31 +347,6 @@ pub fn decode_avatar_bytes(base64_str: &str) -> BichonResult<Vec<u8>> {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn validate_tag(tag: &str) -> Result<(), String> {
|
||||
if tag.is_empty() {
|
||||
return Err("Tag cannot be empty".to_string());
|
||||
}
|
||||
|
||||
const INVALID: &[char] = &[
|
||||
'\'', '"', '`', ';', ',', '(', ')', '[', ']', '{', '}', '<', '>',
|
||||
];
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
for c in tag.chars() {
|
||||
if INVALID.contains(&c) && !found.contains(&c) {
|
||||
found.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
if !found.is_empty() {
|
||||
let chars: String = found.iter().collect();
|
||||
return Err(format!("Tag contains invalid characters: {}", chars));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compute_content_hash(content: &[u8]) -> String {
|
||||
let hash = blake3::hash(content);
|
||||
hash.to_hex().to_string()
|
||||
|
||||
@@ -173,7 +173,28 @@ export function MailListTable({
|
||||
{
|
||||
accessorKey: "subject",
|
||||
header: t('search.subject'),
|
||||
cell: ({ row }) => <LongText className='text-xs'>{row.original.subject}</LongText>,
|
||||
cell: ({ row }) => {
|
||||
const tags = row.original.tags ?? [];
|
||||
return (
|
||||
<div className="flex flex-col gap-1 py-1.5 min-w-0">
|
||||
<LongText className='text-xs font-medium truncate'>
|
||||
{row.original.subject}
|
||||
</LongText>
|
||||
{tags.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center rounded-md bg-primary/15 px-2 py-0.5 text-[10px] font-semibold text-primary ring-1 ring-inset ring-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { className: 'text-left text-xs' }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -88,12 +88,28 @@ export function validateTag(facetPath: string) {
|
||||
};
|
||||
}
|
||||
|
||||
const invalidChars = /['"`;,()[\]{}<>]/;
|
||||
|
||||
if (invalidChars.test(facetPath)) {
|
||||
if (!facetPath.startsWith('/')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Tag path contains invalid characters"
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,6 +117,7 @@ export function validateTag(facetPath: string) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function formatTimestamp(milliseconds: number): string {
|
||||
const date = new Date(milliseconds);
|
||||
const year = date.getFullYear();
|
||||
|
||||
Reference in New Issue
Block a user