courrier preview

This commit is contained in:
alma 2025-05-01 19:55:05 +02:00
parent 3420c5be5c
commit 5a6036b0d5
10 changed files with 56 additions and 708 deletions

View File

@ -6,29 +6,29 @@ This document lists functions and files that have been deprecated and should not
### 1. `lib/email-formatter.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `lib/utils/email-formatter.ts` instead
- **Replacement**: Use `lib/utils/email-utils.ts` instead
- **Reason**: Consolidated email formatting to a single source of truth
### 2. `lib/mail-parser-wrapper.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use functions from `lib/utils/email-formatter.ts` instead
- **Replacement**: Use functions from `lib/utils/email-utils.ts` instead
- **Reason**: Consolidated email formatting and sanitization to a single source of truth
### 3. `lib/email-parser.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `lib/server/email-parser.ts` for parsing and `lib/utils/email-formatter.ts` for sanitization
- **Replacement**: Use `lib/server/email-parser.ts` for parsing and `lib/utils/email-utils.ts` for sanitization
- **Reason**: Consolidated email parsing and formatting to dedicated files
### 4. `lib/compose-mime-decoder.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `decodeComposeContent` and `encodeComposeContent` functions from `lib/utils/email-formatter.ts`
- **Replacement**: Use `decodeComposeContent` and `encodeComposeContent` functions from `lib/utils/email-utils.ts`
- **Reason**: Consolidated MIME handling into the centralized formatter
## Deprecated Functions
### 1. `formatEmailForReplyOrForward` in `lib/services/email-service.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `formatEmailForReplyOrForward` from `lib/utils/email-formatter.ts`
- **Replacement**: Use `formatEmailForReplyOrForward` from `lib/utils/email-utils.ts`
- **Reason**: Consolidated email formatting to a single source of truth
### 2. `formatSubject` in `lib/services/email-service.ts` (REMOVED)
@ -43,7 +43,7 @@ This document lists functions and files that have been deprecated and should not
## Centralized Email Formatting
All email formatting is now handled by the centralized formatter in `lib/utils/email-formatter.ts`. This file contains:
All email formatting is now handled by the centralized formatter in `lib/utils/email-utils.ts`. This file contains:
1. `formatForwardedEmail`: Format emails for forwarding
2. `formatReplyEmail`: Format emails for replying or replying to all
@ -73,36 +73,36 @@ Use these functions for all email formatting needs.
### 4. `cleanHtml` (REMOVED)
- **Location**: Removed from `lib/server/email-parser.ts`
- **Reason**: HTML sanitization has been consolidated in `lib/utils/email-formatter.ts`.
- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-formatter.ts`.
- **Reason**: HTML sanitization has been consolidated in `lib/utils/email-utils.ts`.
- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`.
### 5. `processHtml` (REMOVED)
- **Location**: Removed from `app/api/parse-email/route.ts`
- **Reason**: HTML processing has been consolidated in `lib/utils/email-formatter.ts`.
- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-formatter.ts`.
- **Reason**: HTML processing has been consolidated in `lib/utils/email-utils.ts`.
- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`.
## Deprecated API Routes
### 1. `app/api/mail/[id]/route.ts`
- **Status**: Deleted
### 1. `app/api/mail/[id]/route.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `app/api/courrier/[id]/route.ts` instead.
### 2. `app/api/mail/route.ts`
- **Status**: Deleted
### 2. `app/api/mail/route.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `app/api/courrier/route.ts` instead.
### 3. `app/api/mail/send/route.ts`
- **Status**: Deleted
### 3. `app/api/mail/send/route.ts` (REMOVED)
- **Status**: Removed
- **Replacement**: Use `app/api/courrier/send/route.ts` instead.
## Deprecated Components
### ComposeEmail (components/ComposeEmail.tsx)
### ComposeEmail (components/ComposeEmail.tsx) (REMOVED)
**Status:** Deprecated since November 2023
**Status:** Removed
**Replacement:** Use `components/email/ComposeEmail.tsx` instead
This component has been deprecated in favor of the more modular and better structured version in the email directory. The newer version has the following improvements:
This component has been removed in favor of the more modular and better structured version in the email directory. The newer version has the following improvements:
- Better separation between user message and quoted content in replies/forwards
- Improved styling and visual hierarchy
@ -112,11 +112,6 @@ This component has been deprecated in favor of the more modular and better struc
A compatibility layer has been added to the new component to ensure backward compatibility with existing code that uses the old component. This allows for a smooth transition without breaking changes.
**Migration Plan:**
1. Update imports in files using the old component to import from `@/components/email/ComposeEmail`
2. Test to ensure functionality works with the new component
3. Delete the old component file once all usages have been migrated
## Migration Plan
### Phase 1: Deprecation (Completed)
@ -126,7 +121,7 @@ A compatibility layer has been added to the new component to ensure backward com
### Phase 2: Removal (Completed)
- Remove deprecated files: `lib/email-parser.ts` and `lib/mail-parser-wrapper.ts`
- Consolidate all email formatting in `lib/utils/email-formatter.ts`
- Consolidate all email formatting in `lib/utils/email-utils.ts`
- All email parsing now in `lib/server/email-parser.ts`
- Update documentation to point to the centralized utilities

View File

@ -13,70 +13,70 @@ The application handles email processing through a centralized workflow:
- API route: `/api/parse-email` provides a REST interface to the parser
3. **HTML Sanitization**: Email HTML content is sanitized and processed using:
- `sanitizeHtml` function in `lib/utils/email-formatter.ts` (centralized implementation)
- Text direction (RTL/LTR) is preserved automatically during sanitization
- `sanitizeHtml` function in `lib/utils/email-utils.ts` (centralized implementation)
- DOMPurify with specific configuration to handle email content safely
4. **Email Display**: Sanitized content is rendered in the UI with proper styling and security measures
5. **Email Composition**: The `ComposeEmail` component handles email creation, replying, and forwarding
- Uses the centralized formatter functions to prepare content
- Email is sent through the `/api/courrier/send` endpoint
## Deprecated Functions
## Key Features
Several functions have been deprecated and removed in favor of centralized implementations:
- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements.
- **Email Fetching and Management**: Connect to IMAP servers and manage email fetching and caching logic
- **Email Composition**: Rich text editor with reply and forwarding capabilities
- **Email Display**: Secure rendering of HTML emails
- **Attachment Handling**: View and download attachments
## Project Structure
- `/app` - Main application routes and API endpoints
- `/components` - Reusable React components
- `/lib` - Utility functions and services
The project follows a modular structure:
- `/app` - Next.js App Router structure with routes and API endpoints
- `/components` - React components organized by domain
- `/lib` - Core library code:
- `/server` - Server-only code like email parsing
- `/services` - Domain-specific services, including email service
- `/server` - Server-side utilities
- `/reducers` - State management logic
- `/utils` - Utility functions including the centralized email formatter
## Dependencies
## Technologies
- Next.js 15
- React 18
- ImapFlow for IMAP interactions
- Next.js 14+ with App Router
- React Server Components
- TailwindCSS for styling
- Mailparser for email parsing
- Prisma for database interactions
- Tailwind CSS for styling
- ImapFlow for email fetching
- DOMPurify for HTML sanitization
- Redis for caching
## Development
## State Management
```bash
# Install dependencies
npm install
# Start the development server
npm run dev
# Build for production
npm run build
```
Email state is managed through React context and reducers, with server data fetched through React Server Components or client-side API calls as needed.
# Email Formatting
## Centralized Email Formatter
All email formatting is now handled by a centralized formatter in `lib/utils/email-formatter.ts`. This ensures consistent handling of:
All email formatting is now handled by a centralized formatter in `lib/utils/email-utils.ts`. This ensures consistent handling of:
- Text direction (RTL/LTR)
- Reply and forward formatting
- HTML sanitization
- Content formatting for forwards and replies
- RTL/LTR text direction
- MIME encoding and decoding for email composition
### Key Functions
Key functions include:
- `formatForwardedEmail`: Format emails for forwarding
- `formatReplyEmail`: Format emails for replying
- `sanitizeHtml`: Sanitize HTML while preserving direction attributes
- `sanitizeHtml`: Safely sanitize HTML email content
- `formatEmailForReplyOrForward`: Compatibility function for both
- `decodeComposeContent`: Parse MIME content for email composition
- `encodeComposeContent`: Create MIME-formatted content for sending emails
This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages.
This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages.
## Deprecated Functions
Several functions have been deprecated and removed in favor of centralized implementations:
- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements.

View File

@ -1,99 +0,0 @@
import { NextResponse } from 'next/server';
import { ImapFlow } from 'imapflow';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { prisma } from '@/lib/prisma';
export async function GET(request: Request, { params }: { params: { id: string } }) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Get credentials from database
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
}
});
if (!credentials) {
return NextResponse.json(
{ error: 'No mail credentials found. Please configure your email account.' },
{ status: 401 }
);
}
// Get the current folder from the request URL
const url = new URL(request.url);
const folder = url.searchParams.get('folder') || 'INBOX';
// Connect to IMAP server
const client = new ImapFlow({
host: credentials.host,
port: credentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
},
logger: false,
emitLogs: false,
tls: {
rejectUnauthorized: false
}
});
try {
await client.connect();
await client.mailboxOpen(folder);
// Fetch the full email content
const message = await client.fetchOne(params.id, {
source: true,
envelope: true,
flags: true
});
if (!message) {
return NextResponse.json(
{ error: 'Email not found' },
{ status: 404 }
);
}
// Extract email content
const result = {
id: message.uid.toString(),
from: message.envelope.from[0].address,
subject: message.envelope.subject || '(No subject)',
date: message.envelope.date.toISOString(),
read: message.flags.has('\\Seen'),
starred: message.flags.has('\\Flagged'),
folder: folder,
body: message.source.toString(),
to: message.envelope.to?.map(addr => addr.address).join(', ') || '',
cc: message.envelope.cc?.map(addr => addr.address).join(', ') || '',
bcc: message.envelope.bcc?.map(addr => addr.address).join(', ') || '',
};
return NextResponse.json(result);
} finally {
try {
await client.logout();
} catch (e) {
console.error('Error during logout:', e);
}
}
} catch (error) {
console.error('Error fetching email:', error);
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ status: 500 }
);
}
}

View File

@ -1,172 +0,0 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import Imap from 'imap';
interface StoredCredentials {
email: string;
password: string;
host: string;
port: number;
}
function getStoredCredentials(): StoredCredentials | null {
const cookieStore = cookies();
const credentialsCookie = cookieStore.get('imap_credentials');
if (!credentialsCookie?.value) {
return null;
}
try {
const credentials = JSON.parse(credentialsCookie.value);
if (!credentials.email || !credentials.password || !credentials.host || !credentials.port) {
return null;
}
return credentials;
} catch (error) {
return null;
}
}
export async function POST(request: Request) {
try {
const { emailIds, action } = await request.json();
if (!emailIds || !Array.isArray(emailIds) || !action) {
return NextResponse.json(
{ error: 'Invalid request parameters' },
{ status: 400 }
);
}
// Get the current folder from the request URL
const url = new URL(request.url);
const folder = url.searchParams.get('folder') || 'INBOX';
// Get stored credentials
const credentials = getStoredCredentials();
if (!credentials) {
return NextResponse.json(
{ error: 'No stored credentials found' },
{ status: 401 }
);
}
return new Promise((resolve) => {
const imap = new Imap({
user: credentials.email,
password: credentials.password,
host: credentials.host,
port: credentials.port,
tls: true,
tlsOptions: { rejectUnauthorized: false },
authTimeout: 30000,
connTimeout: 30000
});
const timeout = setTimeout(() => {
console.error('IMAP connection timeout');
imap.end();
resolve(NextResponse.json({ error: 'Connection timeout' }));
}, 30000);
imap.once('error', (err: Error) => {
console.error('IMAP error:', err);
clearTimeout(timeout);
resolve(NextResponse.json({ error: 'IMAP connection error' }));
});
imap.once('ready', () => {
imap.openBox(folder, false, (err, box) => {
if (err) {
console.error(`Error opening box ${folder}:`, err);
clearTimeout(timeout);
imap.end();
resolve(NextResponse.json({ error: `Failed to open folder ${folder}` }));
return;
}
// Convert string IDs to numbers
const numericIds = emailIds.map(id => parseInt(id, 10));
// Process each email
let processedCount = 0;
const totalEmails = numericIds.length;
const processNextEmail = (index: number) => {
if (index >= totalEmails) {
clearTimeout(timeout);
imap.end();
resolve(NextResponse.json({ success: true }));
return;
}
const id = numericIds[index];
const fetch = imap.fetch(id.toString(), {
bodies: '',
struct: true
});
fetch.on('message', (msg) => {
msg.once('attributes', (attrs) => {
const uid = attrs.uid;
if (!uid) {
processedCount++;
processNextEmail(index + 1);
return;
}
switch (action) {
case 'delete':
imap.move(uid, 'Trash', (err) => {
if (err) console.error('Error moving to trash:', err);
processedCount++;
processNextEmail(index + 1);
});
break;
case 'mark-read':
imap.addFlags(uid, ['\\Seen'], (err) => {
if (err) console.error('Error marking as read:', err);
processedCount++;
processNextEmail(index + 1);
});
break;
case 'mark-unread':
imap.removeFlags(uid, ['\\Seen'], (err) => {
if (err) console.error('Error marking as unread:', err);
processedCount++;
processNextEmail(index + 1);
});
break;
case 'archive':
imap.move(uid, 'Archive', (err) => {
if (err) console.error('Error moving to archive:', err);
processedCount++;
processNextEmail(index + 1);
});
break;
}
});
});
fetch.on('error', (err) => {
console.error('Error fetching email:', err);
processedCount++;
processNextEmail(index + 1);
});
};
processNextEmail(0);
});
});
imap.connect();
});
} catch (error) {
console.error('Error in bulk actions:', error);
return NextResponse.json(
{ error: 'Failed to perform bulk action' },
{ status: 500 }
);
}
}

View File

@ -1,135 +0,0 @@
import { NextResponse } from 'next/server';
import { ImapFlow } from 'imapflow';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { prisma } from '@/lib/prisma';
import { saveUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service';
import { prefetchUserEmailData } from '@/lib/services/prefetch-service';
import { invalidateUserEmailCache, getCachedEmailCredentials } from '@/lib/redis';
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
console.log('Session in mail login:', session);
if (!session?.user?.id) {
console.error('No user ID in session');
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Verify user exists
console.log('Checking for user with ID:', session.user.id);
const user = await prisma.user.findUnique({
where: { id: session.user.id }
});
console.log('User found in database:', user);
if (!user) {
console.error('User not found in database');
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
const { email, password, host, port } = await request.json();
if (!email || !password || !host || !port) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Test connection before saving
const connectionSuccess = await testEmailConnection({
email,
password,
host,
port: parseInt(port)
});
if (!connectionSuccess) {
return NextResponse.json(
{ error: 'Failed to connect to email server. Please check your credentials.' },
{ status: 401 }
);
}
// Invalidate all cached data for this user
await invalidateUserEmailCache(session.user.id);
// Save credentials using the service that handles both database and Redis
await saveUserEmailCredentials(session.user.id, {
email,
password,
host,
port: parseInt(port)
});
// Start prefetching email data in the background
prefetchUserEmailData(session.user.id).catch(err => {
console.error('Background prefetch error:', err);
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in login handler:', error);
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ status: 500 }
);
}
}
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// First try to get from Redis cache
let credentials = await getCachedEmailCredentials(session.user.id);
// If not in cache, get from database
if (!credentials) {
credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
},
select: {
email: true,
host: true,
port: true
}
});
} else {
// Remove password from response
const { password, ...safeCredentials } = credentials;
credentials = safeCredentials;
}
if (!credentials) {
return NextResponse.json(
{ error: 'No stored credentials found' },
{ status: 404 }
);
}
return NextResponse.json(credentials);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to retrieve credentials' },
{ status: 500 }
);
}
}

View File

@ -1,91 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { getImapClient } from '@/lib/imap';
import { ImapFlow } from 'imapflow';
export async function POST(request: Request) {
let client: ImapFlow | null = null;
try {
// Get the session and validate it
const session = await getServerSession(authOptions);
console.log('Session:', session); // Debug log
if (!session?.user?.id) {
console.error('No session or user ID found');
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Get the request body
const { emailId, isRead } = await request.json();
console.log('Request body:', { emailId, isRead }); // Debug log
if (!emailId || typeof isRead !== 'boolean') {
console.error('Invalid request parameters:', { emailId, isRead });
return NextResponse.json(
{ error: 'Invalid request parameters' },
{ status: 400 }
);
}
// Get the current folder from the request URL
const url = new URL(request.url);
const folder = url.searchParams.get('folder') || 'INBOX';
console.log('Folder:', folder); // Debug log
try {
// Initialize IMAP client with user credentials
client = await getImapClient();
if (!client) {
console.error('Failed to initialize IMAP client');
return NextResponse.json(
{ error: 'Failed to initialize email client' },
{ status: 500 }
);
}
await client.connect();
await client.mailboxOpen(folder);
// Fetch the email to get its UID
const message = await client.fetchOne(emailId.toString(), {
uid: true,
flags: true
});
if (!message) {
console.error('Email not found:', emailId);
return NextResponse.json(
{ error: 'Email not found' },
{ status: 404 }
);
}
// Update the flags
if (isRead) {
await client.messageFlagsAdd(message.uid.toString(), ['\\Seen'], { uid: true });
} else {
await client.messageFlagsRemove(message.uid.toString(), ['\\Seen'], { uid: true });
}
return NextResponse.json({ success: true });
} finally {
if (client) {
try {
await client.logout();
} catch (e) {
console.error('Error during logout:', e);
}
}
}
} catch (error) {
console.error('Error marking email as read:', error);
return NextResponse.json(
{ error: 'Failed to mark email as read' },
{ status: 500 }
);
}
}

View File

@ -1,56 +0,0 @@
import { NextResponse } from 'next/server';
import Imap from 'imap';
export async function POST(request: Request) {
try {
const { email, password, host, port } = await request.json();
if (!email || !password || !host || !port) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const imapConfig = {
user: email,
password,
host,
port: parseInt(port),
tls: true,
authTimeout: 10000,
connTimeout: 10000,
debug: (info: string) => console.log('IMAP Debug:', info)
};
console.log('Testing IMAP connection with config:', {
...imapConfig,
password: '***',
email
});
const imap = new Imap(imapConfig);
const connectPromise = new Promise((resolve, reject) => {
imap.once('ready', () => {
imap.end();
resolve(true);
});
imap.once('error', (err: Error) => {
imap.end();
reject(err);
});
imap.connect();
});
await connectPromise;
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error testing connection:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to connect to email server' },
{ status: 500 }
);
}
}

View File

@ -1,59 +0,0 @@
import { Mail } from "@/types/mail";
import { Star, StarOff, Paperclip } from "lucide-react";
import { format } from "date-fns";
interface MailListProps {
mails: Mail[];
onMailClick: (mail: Mail) => void;
}
export function MailList({ mails, onMailClick }: MailListProps) {
if (!mails || mails.length === 0) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">No emails found</p>
</div>
);
}
return (
<div className="flex-1 overflow-auto">
{mails.map((mail) => (
<div
key={mail.id}
className={`p-4 border-b cursor-pointer hover:bg-muted ${
!mail.read ? "bg-muted/50" : ""
}`}
onClick={() => onMailClick(mail)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{mail.starred ? (
<Star className="h-4 w-4 text-yellow-400" />
) : (
<StarOff className="h-4 w-4 text-muted-foreground" />
)}
<span className="font-medium">{mail.from}</span>
</div>
<span className="text-sm text-muted-foreground">
{format(new Date(mail.date), "MMM d, yyyy")}
</span>
</div>
<div className="mt-2">
<h3 className="font-medium">{mail.subject}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{mail.body}
</p>
</div>
{mail.attachments && mail.attachments.length > 0 && (
<div className="mt-2 flex items-center text-sm text-muted-foreground">
<Paperclip className="h-3 w-3 mr-1" />
{mail.attachments.length} attachment
{mail.attachments.length > 1 ? "s" : ""}
</div>
)}
</div>
))}
</div>
);
}

View File

@ -1,35 +0,0 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { RefreshCw, Plus, Search } from "lucide-react";
interface MailToolbarProps {
onRefresh: () => void;
onCompose: () => void;
onSearch: (query: string) => void;
}
export function MailToolbar({ onRefresh, onCompose, onSearch }: MailToolbarProps) {
return (
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={onRefresh}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button onClick={onCompose}>
<Plus className="h-4 w-4 mr-2" />
Compose
</Button>
</div>
<div className="flex-1 max-w-md mx-4">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search emails..."
className="pl-8"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
</div>
</div>
);
}

View File

@ -12,7 +12,7 @@ export const useMail = () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/mail');
const response = await fetch('/api/courrier');
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch mails');